Jest Test Suite for a Payment Validator

Write a Jest test suite for a fintech payment validator using TDD.

Introduction

30 Second Summary

Every time you tap "send" on a payment app, dozens of checks run behind the scenes to make sure the money goes to the right place, in the right amount, in a supported currency. If any of those checks have a bug, real money disappears.

In this project, you will build a professional Jest test suite for a fintech payment validator. You will write tests that catch bugs before they reach production, mock an external fraud detection service, and push a complete test suite to GitHub.

What You'll Build

When you run npx jest in your terminal, you will see a full report of passing tests covering transaction validation, edge cases, and mocked fraud detection.

By the end of this project, you'll have:

  • A running unit test suite that validates payment transactions. You can throw any combination of amounts, currencies, and sender/recipient pairs at it and instantly see which ones pass or fail.
  • A test-driven development workflow where you write failing tests that expose real bugs, then fix the code until everything turns green.
  • Mocked async tests that prove your payment processor handles fraud checks, rejections, and service outages without ever calling a live API.
  • Secret Mission: Use data-driven testing with test.each to cover dozens of transaction scenarios in a single test block, and enforce coverage thresholds so your suite fails if gaps appear.

Are there any prerequisites?

No prior testing experience is needed. This guide walks you through everything from installing Jest to pushing your finished test suite to GitHub. You just need Node.js, VS Code, and a GitHub account.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up the Project

Before you can write any tests, you need a project to put them in. This step creates the foundation: a Node.js project with Jest installed as your test runner.

You will also connect your project to a GitHub repository. That way your test suite is version-controlled from the very first commit.

In this step, get ready to:
  • Initialize a Node.js project and install Jest.
  • Connect your project to a GitHub repository.
  • Verify Jest runs from the command line.
Initialize the project and install Jest

Every Node.js project starts with a folder and a package.json file that describes the project and its dependencies. You will create both, then install Jest so it is ready for testing.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir fintech-payment-validator
cd fintech-payment-validator
  • Confirm the folder exists by running:
ls

The output will be empty because the folder is brand new. That is expected.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the fintech-payment-validator folder, then click Open.
  • Open the integrated terminal in VS Code by pressing Ctrl+` (the backtick key, top-left of your keyboard).

Why the integrated terminal?

VS Code's built-in terminal opens automatically in your project folder. You will use it for every command in this project so everything stays in one window.

Now you will create the package.json file that defines your project.

  • Create a new file by clicking File in the top menu bar.
  • Click New File.
  • Save it immediately as package.json by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Paste the following content into package.json:
{
  "name": "fintech-payment-validator",
  "version": "1.0.0",
  "description": "Jest test suite for a fintech payment validator",
  "scripts": {
    "test": "jest"
  },
  "devDependencies": {
    "jest": "30.4.2"
  }
}

What does this file do?

package.json is the identity card for any Node.js project.

  • scripts.test tells npm that when you run npm test, it should execute jest.
  • devDependencies lists packages needed during development. Jest 30.4.2 is pinned here so every run uses the same version.
  • Save package.json with Cmd+S (macOS) or Ctrl+S (Windows).
  • Install the project dependencies by running this in the integrated terminal:
npm install

You should see output showing packages being added, ending with a confirmation that packages were installed.

Seeing permission errors or install failures?

  • Make sure you saved package.json before running npm install. An empty or unsaved file causes errors.
  • Confirm your terminal is inside the fintech-payment-validator folder by running pwd (macOS) or cd (Windows). You should see the folder name at the end of the path.

help me fix npm install errors in my project

Now create the Jest configuration file.

  • Click File in the top menu bar.
  • Click New File.
  • Save it as jest.config.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Paste the following content into jest.config.js:
// Import Jest's config helper
const { defineConfig } = require('jest');

// Export test configuration
module.exports = defineConfig({
  testEnvironment: 'node',
  verbose: true,
});

What does this config do?

  • testEnvironment: 'node' tells Jest to run tests in a Node.js environment instead of a browser-like one.
  • verbose: true makes Jest print the name of each individual test as it runs, so you can see exactly what passed or failed.
  • Save jest.config.js.

Before you run the next command, what version number do you expect to see?

  • Verify Jest is installed at the correct version by running:
npx jest --version

You should see 30.4.2 printed in the terminal. This confirms Jest is installed and accessible.

Not seeing 30.4.2?

  • If you see "command not found", make sure npm install completed successfully. Check for a node_modules folder in your project directory.
  • If you see a different version, delete the node_modules folder and package-lock.json, then run npm install again.

help me get the correct Jest version installed

✔️ Awesome, I've got everything!

Great. Make sure both files are saved and you are ready to connect to GitHub.

ⓧ I'd like to double check the full code

Here is what your package.json should look like:

{
  "name": "fintech-payment-validator",
  "version": "1.0.0",
  "description": "Jest test suite for a fintech payment validator",
  "scripts": {
    "test": "jest"
  },
  "devDependencies": {
    "jest": "30.4.2"
  }
}

Here is what your jest.config.js should look like:

const { defineConfig } = require('jest');

module.exports = defineConfig({
  testEnvironment: 'node',
  verbose: true,
});
Create the Git repo and push to GitHub

Version control tracks every change you make, so you can always rewind if something breaks. You will initialize a Git repository, create a .gitignore to keep generated files out, and push to GitHub.

  • Click File in the top menu bar.
  • Click New File.
  • Save it as .gitignore by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Paste the following content into .gitignore:
node_modules/
coverage/

Why ignore these folders?

node_modules/ contains thousands of dependency files that anyone can regenerate with npm install. Pushing them to GitHub wastes space and clutters your repository.

coverage/ holds test coverage reports that are generated fresh every time you run coverage. They do not belong in version control.

  • Save .gitignore.
  • Initialize a Git repository and make your first commit by running these commands:
git init
git add .
git commit -m "Initial commit: project setup with Jest"

You should see output confirming files were committed, including package.json, jest.config.js, and .gitignore.

Git asking you to configure your identity?

If you see a message about user.email and user.name, Git needs to know who is making commits. Run these two commands with your own details, then retry the commit:

git config --global user.email "your-email@example.com"
git config --global user.name "Your Name"

help me configure Git for the first time

  • Open your browser and go to github.com.
  • Click the + icon in the top-right corner.
  • Click New repository.
  • Enter fintech-payment-validator as the Repository name.
  • Leave the repository set to Public.
  • Do not check any boxes for README, .gitignore, or license (you already have your own files).
  • Click Create repository.

GitHub shows a Quick Setup page with commands. You will use the commands for pushing an existing repository.

  • Connect your local project to GitHub and push by running these commands in your VS Code terminal:
git branch -M main
git remote add origin [[REPO_URL="https://github.com/YOUR-USERNAME/fintech-payment-validator.git"]]
git push -u origin main

You should see output ending with a confirmation that your branch was pushed to the remote.

Push failing with authentication errors?

  • If you see a 403 or authentication error, GitHub may need you to authenticate. Follow the prompts in your terminal or set up a personal access token in your GitHub settings.
  • Make sure you replaced the URL with your own repository URL from the GitHub Quick Setup page.

help me push my code to GitHub

Verify Jest runs

Your project has Jest installed, configured, and pushed to GitHub. The final check is to confirm the test runner actually executes.

Before you run this, what do you think Jest will report when there are no test files to find?

  • Run your test script by executing:
npm test

Jest reports that no tests were found. This is expected because you have not written any test files yet.

Why is this a good sign?

Seeing a "no tests found" message proves that your test script, Jest installation, and configuration all work together correctly. In the next step, you will create test files and see this message change to green passing results.

Seeing a different error instead?

  • If you see a syntax error pointing at jest.config.js, double-check that you used require('jest') and not import. The project uses CommonJS modules.
  • If the command cannot find Jest, the node_modules folder may be missing. Run npm install again.

help me fix Jest not running when I use npm test

✔️ Awesome, I've got everything!

Great. Double check you have saved all files and your GitHub repo shows the committed code.

ⓧ I'd like to double check the full code

Here is what your package.json should look like:

{
  "name": "fintech-payment-validator",
  "version": "1.0.0",
  "description": "Jest test suite for a fintech payment validator",
  "scripts": {
    "test": "jest"
  },
  "devDependencies": {
    "jest": "30.4.2"
  }
}

Here is what your jest.config.js should look like:

const { defineConfig } = require('jest');

module.exports = defineConfig({
  testEnvironment: 'node',
  verbose: true,
});

Here is what your .gitignore should look like:

node_modules/
coverage/

Your test environment is ready and your code is backed up on GitHub. Next up, you will write the payment validator and your first passing tests.

Build the Validator and Write Your First Passing Tests

Your project is wired up and ready to go. Now you need actual code to test.

In this step, you will create a payment validation function and write your first unit tests for the happy path. These tests prove your setup works end-to-end and give you immediate green feedback.

In this step, get ready to:
  • Create a payment validator module that checks transaction amounts and currencies.
  • Write 3 happy-path tests that confirm valid transactions are approved.
  • Run the test suite and see green PASS output.
Create the payment validator module

Every payment system needs a gatekeeper that checks if a transaction makes sense before processing it. Your validator will check two things: that the amount is a number, and that the currency is one your system supports.

  • Create a src folder inside your fintech-payment-validator project by running this command:
mkdir src
  • Create a new file called validateTransaction.js inside the src folder and paste in the following code:
// Currencies our payment system accepts
const SUPPORTED_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY'];
const DAILY_LIMIT = 10000;

function validateTransaction(transaction) {
  // Check that amount is a valid number
  if (typeof transaction.amount !== 'number') {
    return { valid: false, errors: ['Amount must be a number'] };
  }
  // Check that currency is in our supported list
  if (!SUPPORTED_CURRENCIES.includes(transaction.currency)) {
    return { valid: false, errors: [`Currency ${transaction.currency} is not supported`] };
  }
  return { valid: true, errors: [] };
}

module.exports = { validateTransaction, SUPPORTED_CURRENCIES, DAILY_LIMIT };

What does this code do?

This module defines a basic transaction validator with two checks:

  • SUPPORTED_CURRENCIES lists the four currencies your system accepts.
  • DAILY_LIMIT sets the maximum transaction amount (exported for use in tests later).
  • validateTransaction() returns an object with valid: true or valid: false plus an array of error messages.
  • Save src/validateTransaction.js.
  • Verify the module loads correctly by running this command:
node -e "const v = require('./src/validateTransaction'); console.log(v.SUPPORTED_CURRENCIES)"

You should see the array printed in your terminal:

[ 'USD', 'EUR', 'GBP', 'JPY' ]

Seeing 'Cannot find module' error?

  • Make sure your file is saved at src/validateTransaction.js (not directly in the project root).
  • Check you are running the command from the fintech-payment-validator folder, not from inside src/.
  • help me debug this module loading error

✔️ Awesome, I've got everything!

Great. Make sure you have saved your file before moving on.

ⓧ I'd like to double check the full code

const SUPPORTED_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY'];
const DAILY_LIMIT = 10000;

function validateTransaction(transaction) {
  if (typeof transaction.amount !== 'number') {
    return { valid: false, errors: ['Amount must be a number'] };
  }
  if (!SUPPORTED_CURRENCIES.includes(transaction.currency)) {
    return { valid: false, errors: [`Currency ${transaction.currency} is not supported`] };
  }
  return { valid: true, errors: [] };
}

module.exports = { validateTransaction, SUPPORTED_CURRENCIES, DAILY_LIMIT };
Write happy-path tests

Happy-path tests confirm your code works correctly when given valid input. You will write three tests that send properly formed transactions through the validator and confirm they are approved.

  • Create a tests folder inside your project by running this command:
mkdir tests
  • Create a new file called validateTransaction.test.js inside the tests folder and paste in the following code:
const { validateTransaction, DAILY_LIMIT } = require('../src/validateTransaction');

describe('validateTransaction', () => {
  describe('valid transactions', () => {
    // Test 1: a standard valid transaction
    test('approves a valid transaction with all fields', () => {
      const transaction = {
        amount: 100,
        currency: 'USD',
        sender: 'alice@bank.com',
        recipient: 'bob@bank.com',
      };
      const result = validateTransaction(transaction);
      expect(result.valid).toBe(true);
      expect(result.errors).toHaveLength(0);
    });
  });
});

What does this test do?

  • describe() groups related tests together. You have an outer group for the module and an inner group for valid transactions.
  • test() defines a single test case with a readable name.
  • expect().toBe(true) asserts that the result matches what you expect.
  • toHaveLength(0) confirms the errors array is empty for a valid transaction.
  • Now add two more tests below the first test() block, inside the describe('valid transactions') block. Place them after the closing }); of the first test:
    // Test 2: a different supported currency
    test('approves a transaction with EUR currency', () => {
      const transaction = {
        amount: 500,
        currency: 'EUR',
        sender: 'alice@bank.com',
        recipient: 'bob@bank.com',
      };
      const result = validateTransaction(transaction);
      expect(result.valid).toBe(true);
    });

    // Test 3: amount right at the daily limit boundary
    test('approves a transaction at the daily limit', () => {
      const transaction = {
        amount: DAILY_LIMIT,
        currency: 'GBP',
        sender: 'alice@bank.com',
        recipient: 'bob@bank.com',
      };
      const result = validateTransaction(transaction);
      expect(result.valid).toBe(true);
    });

Why these specific test cases?

Each test checks a different aspect of a valid transaction:

  • Test 2 uses EUR to confirm the validator accepts multiple currencies, not just USD.
  • Test 3 uses DAILY_LIMIT (10000) as the amount. Testing at boundaries is a key testing practice because that is where bugs hide.
  • Save tests/validateTransaction.test.js.

✔️ Awesome, I've got everything!

Great. Confirm your file has all three tests inside the nested describe blocks, then save.

ⓧ I'd like to double check the full code

Your test file should contain these pieces in order:

  • The require import at the top pulling in validateTransaction and DAILY_LIMIT.
  • An outer describe('validateTransaction') block wrapping everything.
  • An inner describe('valid transactions') block containing all three tests.
  • Test 1: 'approves a valid transaction with all fields' (USD, amount 100).
  • Test 2: 'approves a transaction with EUR currency' (EUR, amount 500).
  • Test 3: 'approves a transaction at the daily limit' (GBP, amount DAILY_LIMIT).

Scroll up and confirm each chunk is in place, then save the file.

Run the tests and see green

You have a validator and three tests that send valid transactions through it. Time to see if everything connects.

Before you run this, what do you expect to happen? All three transactions are valid, so the validator should approve them.

  • Run your test suite by running this command:
npm test

You should see green PASS output with all 3 tests passing. Jest prints a checkmark next to each test name that passed.

What should I see?

Jest prints PASS followed by the path to your test file. Below that, each test name appears with a green checkmark. At the bottom, you will see a summary showing 3 passed tests and 0 failures.

Tests failing or not found?

  • If you see 'No tests found', check that your test file ends in .test.js and lives inside the tests/ folder.
  • If you see a syntax error, check for missing commas or brackets in your test file. Each test() block needs a closing });.
  • If the import fails, confirm the path is '../src/validateTransaction' (two dots, meaning go up one folder).
  • help me debug why my Jest tests are failing

Three green checkmarks. Your validator works and your test setup is proven. But here is the problem: this validator is dangerously naive. It happily approves negative amounts, zero-dollar transfers, and transactions where someone sends money to themselves. In the next step, you will write tests that expose these bugs.

Edge Case Tests Expose Bugs

Your happy-path tests pass with flying green. The validator approves valid transactions, and your terminal confirms it.

But here is the question: what happens if someone sends a payment for negative $50? Or transfers money to themselves? Right now, the validator silently approves all of these. In this step, you will write tests that expose those gaps. The tests will fail because the validator is not checking for these cases yet. That is the whole point.

In this step, get ready to:
  • Write edge case tests for invalid transactions (negative amounts, zero amounts, missing fields).
  • Run the test suite and observe real failures that prove the validator has bugs.
Write edge case tests for invalid amounts

In a real payment system, a transaction for -$50 or $0 should never go through. Neither should a transaction that exceeds the daily limit. Your current validator does not check for any of these cases.

  • In tests/validateTransaction.test.js, find the closing }); of the describe('valid transactions') block.
  • Add this new describe block directly below it, still inside the outer describe('validateTransaction') block:
  describe('invalid transactions - edge cases', () => {
    test('rejects a negative amount', () => {
      const transaction = { amount: -50, currency: 'USD', sender: 'alice@bank.com', recipient: 'bob@bank.com' };
      const result = validateTransaction(transaction);
      expect(result.valid).toBe(false);
      expect(result.errors).toContain('Amount must be greater than zero');
    });

    test('rejects a zero amount', () => {
      const transaction = { amount: 0, currency: 'USD', sender: 'alice@bank.com', recipient: 'bob@bank.com' };
      const result = validateTransaction(transaction);
      expect(result.valid).toBe(false);
      expect(result.errors).toContain('Amount must be greater than zero');
    });

    test('rejects an amount exceeding the daily limit', () => {
      const transaction = { amount: DAILY_LIMIT + 1, currency: 'USD', sender: 'alice@bank.com', recipient: 'bob@bank.com' };
      const result = validateTransaction(transaction);
      expect(result.valid).toBe(false);
      expect(result.errors).toContain(`Amount exceeds daily limit of ${DAILY_LIMIT}`);
    });

What does this code do?

Each test creates a transaction with a specific invalid amount, runs it through the validator, and asserts the result should be valid: false with a matching error message.

  • The negative and zero tests catch transactions that should never exist in a real payment system.
  • The daily limit test uses the exported DAILY_LIMIT constant (10000) plus 1, so it stays in sync if the limit ever changes.
Add tests for missing fields and identity checks

Amount is not the only thing that can go wrong. A transaction could be missing its currency entirely, use an unsupported currency, or even try to send money from someone to themselves.

  • Directly below the daily limit test you just added (still inside the same describe('invalid transactions - edge cases') block), add these tests:
    test('rejects a transaction with missing currency', () => {
      const transaction = { amount: 100, sender: 'alice@bank.com', recipient: 'bob@bank.com' };
      const result = validateTransaction(transaction);
      expect(result.valid).toBe(false);
      expect(result.errors).toContain('Currency is required');
    });

    test('rejects a transaction with unsupported currency', () => {
      const transaction = { amount: 100, currency: 'BTC', sender: 'alice@bank.com', recipient: 'bob@bank.com' };
      const result = validateTransaction(transaction);
      expect(result.valid).toBe(false);
      expect(result.errors).toContain('Currency BTC is not supported');
    });

    test('rejects when sender equals recipient', () => {
      const transaction = { amount: 100, currency: 'USD', sender: 'alice@bank.com', recipient: 'alice@bank.com' };
      const result = validateTransaction(transaction);
      expect(result.valid).toBe(false);
      expect(result.errors).toContain('Sender and recipient cannot be the same');
    });

    test('rejects a null transaction', () => {
      const result = validateTransaction(null);
      expect(result.valid).toBe(false);
      expect(result.errors).toContain('Transaction object is required');
    });
  });

What does this code do?

  • The missing currency test omits the currency field entirely and expects a specific 'Currency is required' error.
  • The unsupported currency test uses 'BTC' which is not in the supported list.
  • The sender/recipient test catches self-transfers, which are a common fraud vector.
  • The null test verifies the validator does not crash if it receives no transaction at all.
  • Save tests/validateTransaction.test.js.

Seeing a syntax error in your editor?

Make sure the new describe('invalid transactions - edge cases') block is inside the outer describe('validateTransaction') block, not outside it. The closing }); of the outer block should be the very last line of the file.

Check that you have matching opening and closing braces for every test() and describe() call.

Help me debug a syntax error in my Jest test file.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your file before moving on.

ⓧ I'd like to double check the full code

Your tests/validateTransaction.test.js should have two describe blocks inside the outer describe('validateTransaction'):

  • The original describe('valid transactions') block with 3 tests (valid USD, valid EUR, valid at daily limit).
  • The new describe('invalid transactions - edge cases') block with 7 tests (negative, zero, exceeding limit, missing currency, unsupported currency, sender=recipient, null).

Scroll up and confirm each chunk is present, in order. The file should have 10 total tests across the two describe blocks.

Run the tests and observe the failures

Your 7 new tests assert that the validator should reject bad transactions. But the current validator only checks two things: whether amount is a number and whether currency is supported. It has no rules for negative amounts, zero amounts, limits, missing fields, or self-transfers.

Before you run the tests, predict: how many of these 7 new tests do you think will pass? Will most fail, or will some accidentally pass?

  • Run the test suite:
npm test

You will see red FAIL output. Multiple tests report that they expected valid: false but received valid: true. The validator happily approved a negative $50 payment, a $0 transfer, and a transaction where Alice sends money to herself.

This is exactly the result you want. These are real bugs, and your tests just caught them before they reached production.

Why is failing a good thing here?

This is the core value of testing. A payment validator that silently approves invalid transactions could cost a company real money. Your tests act as a bug report. Each red failure line tells you exactly what the validator is missing.

In the next step, you will fix the validator one rule at a time until every test turns green. That is the red-green TDD cycle.

Your tests have done their job. They found the bugs. Next up, you will fix the validator one rule at a time until every single test glows green.

Fix the Validator Using the TDD Cycle

In the last step, you saw your tests light up red. Those failures are not a problem. They are your bug report.

Now you fix the validator one rule at a time until every test passes. This is the red-green TDD cycle that test engineers use daily: write a failing test (red), then write the minimum code to make it pass (green).

In this step, get ready to:
  • Add validation rules for amounts, required fields, and sender/recipient checks.
  • Run the tests and see every one pass green.
  • Commit the fix to Git.
Add validation rules for amount and required fields

The naive validator only checks whether the amount is a number and whether the currency is in the supported list. It has no idea what to do with negative amounts, missing fields, or a sender paying themselves.

  • Open src/validateTransaction.js in VS Code.
  • Select all the existing code and replace it with the following:
const SUPPORTED_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY'];
const DAILY_LIMIT = 10000;

function validateTransaction(transaction) {
  const errors = [];

  // Handle null/undefined transaction
  if (!transaction) {
    return { valid: false, errors: ['Transaction object is required'] };
  }

  // Amount validation chain
  if (transaction.amount === undefined || transaction.amount === null) {
    errors.push('Amount is required');
  } else if (typeof transaction.amount !== 'number') {
    errors.push('Amount must be a number');
  } else if (transaction.amount <= 0) {
    errors.push('Amount must be greater than zero');
  } else if (transaction.amount > DAILY_LIMIT) {
    errors.push(`Amount exceeds daily limit of ${DAILY_LIMIT}`);
  }

  // Currency validation
  if (!transaction.currency) {
    errors.push('Currency is required');
  } else if (!SUPPORTED_CURRENCIES.includes(transaction.currency)) {
    errors.push(`Currency ${transaction.currency} is not supported`);
  }

What does this code do?

Instead of returning immediately on the first error, the new approach collects errors into an array. This lets the validator report multiple problems at once.

  • The if (!transaction) guard returns early for null inputs so the rest of the checks don't throw.
  • The amount chain uses else if so only the first failing condition fires. A negative amount does not also trigger the type check.
  • Directly below the currency validation you just pasted, add the remaining checks:
  // Sender and recipient validation
  if (!transaction.sender) {
    errors.push('Sender is required');
  }

  if (!transaction.recipient) {
    errors.push('Recipient is required');
  }

  if (transaction.sender && transaction.recipient && transaction.sender === transaction.recipient) {
    errors.push('Sender and recipient cannot be the same');
  }

  // Return result
  if (errors.length > 0) {
    return { valid: false, errors };
  }

  return { valid: true, errors: [] };
}

module.exports = { validateTransaction, SUPPORTED_CURRENCIES, DAILY_LIMIT };

What does this code do?

  • The sender and recipient checks use !transaction.sender which catches both missing and empty-string values.
  • The same-sender-recipient check only runs when both fields exist. This avoids a false positive when one is missing.
  • At the end, if the errors array is empty the transaction is valid. Otherwise, the array carries every reason for rejection.
  • Save src/validateTransaction.js.

Seeing a syntax error when you save?

  • Make sure the closing } of the function appears before the module.exports line. A missing brace is the most common cause.
  • Check that you removed ALL the old function body. Leftover lines from the naive version will conflict with the new code.

Why is my validateTransaction.js showing a syntax error after I replaced the code?

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

Your file has two sections you pasted above. Scroll up and confirm each one is present, in this order:

  • The constants (SUPPORTED_CURRENCIES and DAILY_LIMIT), the function opening with the errors array, the null check, the amount chain, and the currency checks.
  • The sender check, recipient check, same-sender-recipient check, the final if (errors.length > 0) return, the return { valid: true, errors: [] } line, the closing brace, and the module.exports line.
Run tests and confirm all green

Every validation rule now has code behind it. Before you run the tests, think about what you expect to see. Do you think all 10 tests will pass, or will some still fail?

  • Run the test suite by running this command:
npm test

You should see green PASS output with all 10 tests passing. Both the happy-path tests and the edge case tests now show green checkmarks.

What is the red-green cycle?

You just completed one full cycle of test-driven development. In Step 3, the tests went red (failing). In this step, you wrote the code to turn them green (passing). In a real fintech team, this loop repeats dozens of times a day.

Still seeing test failures?

  • Read the error message closely. It shows the expected value vs. the received value. Match the error string in your code to the one the test expects (case-sensitive, exact punctuation).
  • If the null transaction test fails with a TypeError, confirm your if (!transaction) guard is the very first check inside the function, before any transaction.amount access.

My tests are still failing after I updated the validator. Can you help me compare my code to what the tests expect?

  • Commit your fix by running these commands:
git add .
git commit -m "fix: add validation rules for edge cases"

Why this commit message style?

The fix: prefix follows the Conventional Commits format. It tells other developers at a glance that this commit repairs broken behaviour rather than adding new features.

Your validator is now battle-tested against every edge case your tests could throw at it. Next up, you will mock an external fraud detection API so you can test async payment processing without hitting a live service.

Mock an External Fraud Check API

Your validator catches bad transactions before they leave the building. But in a real payment system, valid transactions still need to pass through an external fraud detection service before they get approved.

You cannot hit a real fraud API inside your tests. It would be slow, cost money, and return unpredictable results. jest.mock solves this by replacing the real module with a fake version you control. You get to simulate approved transactions, flagged transactions, and even service outages.

In this step, get ready to:
  • Create a fraud check module and a payment processor that uses it.
  • Write async tests using jest.mock to simulate fraud API responses.
  • Run the full test suite and push everything to GitHub.
Create the fraud check module and payment processor

The fraud check module represents an external API your payment system calls. The payment processor orchestrates the full flow: validate first, then check for fraud.

  • Create a new file src/fraudCheck.js by running this command:
touch src/fraudCheck.js
  • Open src/fraudCheck.js in VS Code and paste the following code:
// Simulates calling an external fraud detection API
async function checkFraud(transaction) {
  const response = await fetch('https://api.fraudservice.example/check', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(transaction),
  });

  const result = await response.json();
  return result;
}

module.exports = { checkFraud };

What does this code do?

This module wraps a POST request to a fraud detection endpoint. In production, this would call a real third-party service that scores transactions for risk.

You will never actually call this endpoint in tests. jest.mock will replace the entire module with a fake, so the real fetch call never fires.

  • Save src/fraudCheck.js.
  • Confirm the file exists by running:
ls src/

You should see fraudCheck.js alongside validateTransaction.js in the output.

Now you need a payment processor that ties validation and fraud checking together.

  • Create src/paymentProcessor.js by running:
touch src/paymentProcessor.js
  • Open src/paymentProcessor.js and paste this first section, which handles imports and validation:
const { validateTransaction } = require('./validateTransaction');
const { checkFraud } = require('./fraudCheck');

async function processPayment(transaction) {
  // Run validation first - reject immediately if invalid
  const validation = validateTransaction(transaction);

  if (!validation.valid) {
    return {
      status: 'rejected',
      reason: 'validation_failed',
      errors: validation.errors,
    };
  }

What does this code do?

The processor reuses your validateTransaction function as a gatekeeper. If the transaction is invalid, it gets rejected immediately without ever calling the fraud service. This saves time and money on real API calls.

  • Directly below the code you just pasted (still inside the processPayment function), add the fraud check logic and export:
  // Only call fraud service for valid transactions
  try {
    const fraudResult = await checkFraud(transaction);

    if (fraudResult.flagged) {
      return {
        status: 'rejected',
        reason: 'fraud_detected',
        riskScore: fraudResult.riskScore,
      };
    }

    return {
      status: 'approved',
      transactionId: `TXN-${Date.now()}`,
    };
  } catch (error) {
    // Handle fraud service being unavailable
    return {
      status: 'error',
      reason: 'fraud_check_unavailable',
      message: error.message,
    };
  }
}

module.exports = { processPayment };

What does this code do?

  • The try/catch block calls the fraud service and handles three outcomes: clean (approved), flagged (rejected with a risk score), or service error (graceful failure).
  • The transactionId uses a timestamp prefix so every approved transaction gets a unique ID.
  • If the fraud service is down, the processor returns an error status instead of crashing. This is a common resilience pattern in payment systems.
  • Save src/paymentProcessor.js.
  • Run your existing tests to confirm the new files do not break anything:
npm test

You should still see PASS with all 10 existing tests green. The new modules exist but are not tested yet.

Seeing a syntax error?

  • Make sure the second code block in paymentProcessor.js sits directly below the first, inside the same function. There should be no duplicate function processPayment declaration.
  • Check that the closing } for the function and the module.exports line are both present at the bottom.

help me fix a syntax error in my paymentProcessor.js file

✔️ Awesome, I've got everything!

Great. Double check you have saved both src/fraudCheck.js and src/paymentProcessor.js.

ⓧ I'd like to double check the full code

Here is the complete src/fraudCheck.js:

async function checkFraud(transaction) {
  const response = await fetch('https://api.fraudservice.example/check', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(transaction),
  });

  const result = await response.json();
  return result;
}

module.exports = { checkFraud };

For src/paymentProcessor.js, confirm you have the following sections in order:

  • Two require imports at the top (validateTransaction and checkFraud).
  • The async function processPayment declaration.
  • A validation check that returns early with status: 'rejected' and reason: 'validation_failed'.
  • A try/catch block that calls checkFraud, handles flagged results, returns approved, or catches errors.
  • module.exports = { processPayment } at the bottom.
Write tests with jest.mock for the fraud check

jest.mock replaces an entire module with an auto-mocked version. Every exported function becomes a jest.fn() that you can program to return whatever you want. This means your tests never touch the network.

  • Create the test file by running:
touch tests/paymentProcessor.test.js
  • Open tests/paymentProcessor.test.js and paste this first section with the imports, mock setup, and first test:
const { processPayment } = require('../src/paymentProcessor');
const { checkFraud } = require('../src/fraudCheck');

// Replace the real fraudCheck module with a mock
jest.mock('../src/fraudCheck');

describe('processPayment', () => {
  // Reset mock state between tests
  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('approves a valid transaction that passes fraud check', async () => {
    // Program the mock to return a clean result
    checkFraud.mockResolvedValue({ flagged: false, riskScore: 0.1 });

    const transaction = {
      amount: 250,
      currency: 'USD',
      sender: 'alice@bank.com',
      recipient: 'bob@bank.com',
    };

    const result = await processPayment(transaction);

    expect(result.status).toBe('approved');
    expect(result.transactionId).toMatch(/^TXN-/);
    expect(checkFraud).toHaveBeenCalledWith(transaction);
    expect(checkFraud).toHaveBeenCalledTimes(1);
  });

What does this code do?

  • jest.mock('../src/fraudCheck') tells Jest to replace the real module. Every function in it becomes a controllable mock.
  • beforeEach with jest.clearAllMocks() resets call counts and return values before each test, so tests do not leak state into each other.
  • mockResolvedValue programs the mock to resolve its promise with the object you provide. The real fetch call never fires.
  • Save tests/paymentProcessor.test.js and run the tests to confirm the first mock test works:
npm test

You should see PASS for both test files. The new processPayment test appears alongside your existing validator tests.

Test failing with a network error?

If you see an error about fetch or a network request, the jest.mock line might be missing or misspelled. Check that it says jest.mock('../src/fraudCheck') exactly, matching the file path.

help me fix my jest.mock setup for the fraud check module

  • Now add the second and third tests directly below the first test (still inside the describe block):
  test('rejects a transaction flagged for fraud', async () => {
    // Simulate the fraud service flagging this transaction
    checkFraud.mockResolvedValue({ flagged: true, riskScore: 0.95 });

    const transaction = {
      amount: 9000,
      currency: 'USD',
      sender: 'alice@bank.com',
      recipient: 'bob@bank.com',
    };

    const result = await processPayment(transaction);

    expect(result.status).toBe('rejected');
    expect(result.reason).toBe('fraud_detected');
    expect(result.riskScore).toBe(0.95);
  });

  test('rejects an invalid transaction without calling fraud check', async () => {
    const transaction = {
      amount: -100,
      currency: 'USD',
      sender: 'alice@bank.com',
      recipient: 'bob@bank.com',
    };

    const result = await processPayment(transaction);

    expect(result.status).toBe('rejected');
    expect(result.reason).toBe('validation_failed');
    expect(checkFraud).not.toHaveBeenCalled();
  });

What do these tests prove?

  • The fraud-flagged test proves your processor correctly rejects high-risk transactions and surfaces the risk score.
  • The invalid transaction test proves the processor short-circuits. It never wastes a fraud API call on a transaction that already failed validation. The expect(checkFraud).not.toHaveBeenCalled() assertion verifies this.
  • Add the final test and closing brace directly below:
  test('returns error when fraud check service is unavailable', async () => {
    // Simulate the fraud service throwing an error
    checkFraud.mockRejectedValue(new Error('Service timeout'));

    const transaction = {
      amount: 500,
      currency: 'EUR',
      sender: 'alice@bank.com',
      recipient: 'bob@bank.com',
    };

    const result = await processPayment(transaction);

    expect(result.status).toBe('error');
    expect(result.reason).toBe('fraud_check_unavailable');
    expect(result.message).toBe('Service timeout');
  });
});

What does mockRejectedValue do?

mockRejectedValue programs the mock to reject its promise, simulating a crashed service. This test proves your processor handles outages gracefully instead of crashing the whole payment flow.

  • Save tests/paymentProcessor.test.js.

✔️ Awesome, I've got everything!

Great. Confirm you have saved the file with all four tests inside the describe block.

ⓧ I'd like to double check the full code

Confirm your tests/paymentProcessor.test.js contains these sections in order:

  • Two require imports (processPayment and checkFraud).
  • jest.mock('../src/fraudCheck') call.
  • A describe('processPayment') block wrapping everything.
  • A beforeEach calling jest.clearAllMocks().
  • Test 1: approves a valid transaction (uses mockResolvedValue with flagged: false).
  • Test 2: rejects fraud-flagged transaction (uses mockResolvedValue with flagged: true).
  • Test 3: rejects invalid transaction and checkFraud is not.toHaveBeenCalled().
  • Test 4: returns error when service is unavailable (uses mockRejectedValue).
  • Closing }); for the describe block.
Run all tests and push to GitHub

Before you run the full suite, think about what you expect to see. You have 10 validator tests from earlier and 4 new payment processor tests.

  • Run the full test suite:
npm test

You should see PASS for both test files with all 14 tests green. The payment processor tests prove your mock is working because they run instantly without any network calls.

Seeing test failures?

  • If the fraud-flagged test fails, check that your processPayment function checks fraudResult.flagged and returns riskScore: fraudResult.riskScore.
  • If the unavailable test fails, check that your catch block returns message: error.message (not error directly).

help me debug my payment processor tests that are failing

  • Commit and push your work to GitHub by running these commands:
git add .
git commit -m "feat: add payment processor with mocked fraud check tests"
git push

Why mock instead of stub?

Jest mocks replace entire modules at the import level. This is more powerful than manually stubbing a single function because it guarantees no code path can accidentally reach the real implementation. In fintech, an accidental live API call during testing could trigger real fraud alerts or charge real money.

Your test suite now covers validation logic, edge cases, and async API interactions. You have proven you can test code that depends on external services without touching the network. Next up, the Secret Mission challenges you to scale this approach with data-driven testing.

Secret mission

Data-Driven Testing and Coverage Thresholds

In fintech, you need to test dozens of transaction scenarios without writing a separate test for each one. Can you use test.each to cover 8 scenarios in a single test block, and enforce coverage thresholds so your suite fails if quality drops?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • Local project folder (fintech-payment-validator on your Desktop).
  • GitHub repository for your test suite.
  • Node.js packages installed in node_modules/.

Keep everything running

No action needed. Choose this if you're still actively building or want to revisit your test suite later.

  • Your local fintech-payment-validator folder stays on your Desktop at no cost.
  • Your GitHub repository remains available for free under your account.
  • Jest and your node_modules/ stay installed inside the project folder.

Pause - I'll come back to this later

There are no running processes or servers to stop in this project. Your files sit quietly on disk and GitHub until you return.

  • Nothing to pause. Your project folder and GitHub repo are already idle.
  • If you want to free up disk space without fully deleting, you can remove node_modules/ and reinstall later with npm install.

Delete - I don't want to use this again

Remove all project resources and start fresh. This permanently deletes your local files and GitHub repository.

  • Delete your local project folder by running this command in your terminal:

macOS

cd ~/Desktop
rm -rf fintech-payment-validator

Windows

cd ~\Desktop
rmdir /s /q fintech-payment-validator
  • Delete your GitHub repository by going to your repository page on GitHub.
  • Click Settings in the top navigation bar.
  • Scroll to the bottom of the page to the Danger Zone section.
  • Click Delete this repository and follow the confirmation prompts.

Nice Work!

Nice Work!

You just built a professional test suite for a fintech payment validator from scratch. Your GitHub repository now proves you can write the kind of automated tests that prevent costly bugs in real payment systems.

You've learned how to:

  • Write unit tests with Jest that validate transaction amounts, currencies, and required fields, and use the red-green TDD cycle to expose bugs with failing tests and fix them until all tests pass.
  • Mock an external fraud detection API using jest.mock with mockResolvedValue and mockRejectedValue to test async payment processing without hitting a live service.
  • Set up a complete Node.js project with a test script, configuration, and a GitHub repository that employers can review.
  • Secret Mission: Used test.each for data-driven testing across dozens of transaction scenarios and enforced coverage thresholds so the suite fails if coverage drops below 80%.

Ready to quiz yourself?