JavaScript Testing Basics
Testing means writing code that automatically checks whether your other code works correctly. Instead of clicking through your app manually every time you make a change, tests run in seconds and catch broken functionality instantly. Good tests are the safety net that lets you refactor and add features with confidence.
Why Write Tests?
Imagine building a calculator. You add a new feature — currency conversion — and accidentally break the basic addition. Without tests, you might not notice until a user reports it. With tests, the broken addition test fails within seconds of your change.
Diagram: Test as a Safety Net
Without tests: Change code → manually click through app → miss bug → user reports it With tests: Change code → tests run automatically → broken test fails instantly → fix it before it ever reaches users
Types of Tests
| Type | Tests What | Example |
|---|---|---|
| Unit Test | A single function or class | Does add(2, 3) return 5? |
| Integration Test | Multiple parts working together | Does login + redirect work end to end? |
| End-to-End (E2E) | Full user journey in a real browser | Can a user sign up, log in, and place an order? |
The Anatomy of a Test
Every test follows the AAA pattern: Arrange, Act, Assert.
Diagram: AAA Pattern
Arrange → set up the data and conditions
Act → run the function or action
Assert → check that the result is what you expected
Example:
Arrange: let a = 2, b = 3
Act: let result = add(a, b)
Assert: result should equal 5
Writing Tests Without a Framework
You can write basic tests in plain JavaScript to understand what a testing framework does behind the scenes.
// The function to test
function add(a, b) {
return a + b;
}
// A simple test helper
function expect(value) {
return {
toBe(expected) {
if (value === expected) {
console.log("PASS ✓");
} else {
console.error("FAIL ✗ Expected:", expected, "Got:", value);
}
}
};
}
// Tests
expect(add(2, 3)).toBe(5); // PASS ✓
expect(add(0, 0)).toBe(0); // PASS ✓
expect(add(-1, 1)).toBe(0); // PASS ✓
expect(add(2, 3)).toBe(99); // FAIL ✗ Expected: 99 Got: 5
Introduction to Jest
Jest is the most popular JavaScript testing framework. It provides everything you need: a test runner, assertion functions, and mocking tools — all in one package.
Install Jest
npm install --save-dev jest
Add to package.json
{
"scripts": {
"test": "jest"
}
}
Run Tests
npm test
Writing Your First Jest Test
// math.js — the code to test
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
function multiply(a, b) { return a * b; }
module.exports = { add, subtract, multiply };
// math.test.js — the test file
const { add, subtract, multiply } = require("./math");
test("add: 2 + 3 should equal 5", function() {
expect(add(2, 3)).toBe(5);
});
test("subtract: 10 - 4 should equal 6", function() {
expect(subtract(10, 4)).toBe(6);
});
test("multiply: 3 × 4 should equal 12", function() {
expect(multiply(3, 4)).toBe(12);
});
Jest Output
PASS ./math.test.js ✓ add: 2 + 3 should equal 5 (2ms) ✓ subtract: 10 - 4 should equal 6 (0ms) ✓ multiply: 3 × 4 should equal 12 (0ms) Tests: 3 passed, 3 total
Common Jest Matchers
Matchers are the assertion methods chained to expect().
// Equality
expect(1 + 1).toBe(2); // strict equal (===)
expect({ a: 1 }).toEqual({ a: 1 }); // deep equal (for objects/arrays)
// Truthiness
expect(true).toBeTruthy();
expect(null).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
// Numbers
expect(10).toBeGreaterThan(5);
expect(3).toBeLessThan(10);
expect(3.14).toBeCloseTo(3.1, 1); // floating point comparison
// Strings
expect("Hello World").toContain("World");
expect("test@mail.com").toMatch(/@/); // regex match
// Arrays
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
// Errors
expect(() => JSON.parse("bad")).toThrow();
expect(() => JSON.parse("bad")).toThrow(SyntaxError);
Grouping Tests with describe
Group related tests inside a describe block to organise your test file.
describe("Calculator functions", function() {
describe("add", function() {
test("adds two positive numbers", () => {
expect(add(3, 4)).toBe(7);
});
test("adds negative and positive", () => {
expect(add(-3, 3)).toBe(0);
});
});
describe("multiply", function() {
test("multiplies two numbers", () => {
expect(multiply(3, 4)).toBe(12);
});
test("multiplies by zero", () => {
expect(multiply(5, 0)).toBe(0);
});
});
});
Diagram: describe + test Structure
describe("Calculator functions")
│
├─ describe("add")
│ ├─ test("adds positives")
│ └─ test("adds negatives")
│
└─ describe("multiply")
├─ test("multiplies two numbers")
└─ test("multiplies by zero")
Testing Async Code
// Async function to test
async function fetchData(id) {
let response = await fetch("https://jsonplaceholder.typicode.com/posts/" + id);
return response.json();
}
// Test with async/await
test("fetches post with id 1", async function() {
let post = await fetchData(1);
expect(post.id).toBe(1);
expect(post).toHaveProperty("title");
});
setup and Teardown
Run code before or after tests using beforeEach, afterEach, beforeAll, and afterAll.
let cart;
beforeEach(function() {
// Reset cart before every test
cart = [];
});
test("cart starts empty", function() {
expect(cart).toHaveLength(0);
});
test("can add item to cart", function() {
cart.push({ name: "Laptop", price: 50000 });
expect(cart).toHaveLength(1);
expect(cart[0].name).toBe("Laptop");
});
What Makes a Good Test
- Tests one specific thing — not five things in a single test.
- Has a clear, descriptive name that explains what it checks.
- Does not depend on the order tests run.
- Runs fast — unit tests should finish in milliseconds.
- Fails clearly — when it breaks, you know exactly why.
Summary
Testing automatically verifies that your JavaScript code works as expected. Unit tests check individual functions, integration tests check combined parts, and end-to-end tests check full user flows. The AAA pattern — Arrange, Act, Assert — structures every test. Jest provides test(), expect(), and matchers like toBe, toEqual, and toThrow to write and run tests with a simple command. Write tests alongside your code — they catch bugs early and give you confidence to improve your codebase without fear of breaking things.
