Testing JavaScript
Testing means checking that your code does what you expect it to do. Think of it like test-driving a car before buying it — you want to make sure everything works before you rely on it.
Without tests, bugs can slip into production. Small changes can break other parts of your app. You end up spending hours debugging problems that a simple test would have caught immediately.
With tests, you catch bugs early. Code changes feel much safer. Your teammates can also read your tests to understand exactly what each piece of code is supposed to do.
Even a simple test can save hours of debugging:
// A simple function
function add(a, b) {
return a + b;
}
// A simple test (without any library)
console.assert(add(2, 3) === 5, "add(2, 3) should return 5");
console.assert(add(-1, 1) === 0, "add(-1, 1) should return 0");
console.log("All tests passed!");
There are three main types of testing in JavaScript. Each one checks a different level of your application.
Unit Testing tests one small piece — usually a single function — in isolation. Think of it like checking that each individual LEGO brick is the right shape before you start building. It is fast and focused.
Integration Testing tests how multiple pieces work together. Like checking that LEGO bricks fit together properly once you start assembling them. It catches problems that only appear when things interact.
End-to-End (E2E) Testing tests the whole application like a real user would — clicking buttons, filling out forms, and checking the results. Like test-driving the finished LEGO car. It is the most realistic but also the slowest.
For beginners, unit testing is the best place to start. Here is a quick summary:
- Unit tests — fast, isolated, tests one function at a time
- Integration tests — slower, tests multiple units working together
- E2E tests — slowest, tests the full user flow in a real browser
Popular tools: Jest is used for unit and integration testing. Cypress and Playwright are popular for E2E testing.
A unit test checks one function or one piece of behaviour at a time. Unit tests are fast to run, easy to write, and isolated — they do not need a database, a network connection, or a running server.
The best way to structure a unit test is the Arrange → Act → Assert pattern:
- Arrange — set up the data and variables you need
- Act — call the function you are testing
- Assert — check that the result is what you expected
// Function to test
function multiply(a, b) {
return a * b;
}
// Unit test (manual, no library)
function testMultiply() {
// Arrange
const a = 4;
const b = 5;
// Act
const result = multiply(a, b);
// Assert
if (result === 20) {
console.log("✅ multiply(4, 5) passed");
} else {
console.error("❌ multiply(4, 5) failed — expected 20, got " + result);
}
}
testMultiply();
The Arrange → Act → Assert pattern is the foundation of all unit testing, regardless of which library you use. A good unit test checks only one thing, has a clear description, and gives the same result every time you run it.
Jest is the most popular JavaScript testing library. It was made by Facebook and is used by millions of developers. It gives you a clean, readable syntax for writing tests and tells you clearly which tests passed and which failed.
First, install Jest in your project:
npm install --save-dev jest
Test files should end in .test.js. Here is a basic example:
// math.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// math.test.js
const { add } = require('./math');
test('adds 2 + 3 to equal 5', () => {
expect(add(2, 3)).toBe(5);
});
test('adds negative numbers', () => {
expect(add(-1, -2)).toBe(-3);
});
Key Jest concepts:
- test() or it() — defines a single test case
- expect() — the value you want to check
- .toBe() — checks strict equality (like ===)
- .toEqual() — checks deep equality for objects and arrays
- .toBeTruthy() / .toBeFalsy() — checks truthy or falsy values
Run your tests with:
npx jest
Jest gives you clear output — it shows which tests passed (✅) and which failed (❌) so you can fix problems quickly.
Let us walk through a real example step by step. Imagine you have a function that checks whether a number is even:
// isEven.js
function isEven(number) {
return number % 2 === 0;
}
module.exports = { isEven };
Now write a test file for it:
// isEven.test.js
const { isEven } = require('./isEven');
describe('isEven', () => {
test('returns true for even numbers', () => {
expect(isEven(4)).toBe(true);
expect(isEven(0)).toBe(true);
});
test('returns false for odd numbers', () => {
expect(isEven(3)).toBe(false);
expect(isEven(7)).toBe(false);
});
test('handles negative numbers', () => {
expect(isEven(-2)).toBe(true);
expect(isEven(-3)).toBe(false);
});
});
describe() groups related tests together — think of it like a folder for your test cases. It makes the output easier to read when you have many tests.
Tips for writing good tests:
- Test the happy path — normal input that should work
- Test edge cases — zero, negative numbers, empty strings
- Test what happens with unexpected input
- Give your tests a clear description of exactly what they check
TDD means writing your test first, then writing the code to make it pass. This sounds backwards, but it works really well in practice.
Think of it like writing a shopping list before going to the store — you know exactly what you need before you start. TDD forces you to think about what your code should do before you write a single line of it.
The TDD cycle has three steps:
- 🔴 Red — write a test that fails (because the code does not exist yet)
- 🟢 Green — write just enough code to make the test pass
- 🔵 Refactor — clean up the code without breaking the test
Here is an example using a simple capitalize function:
// Step 1: Write the test first (RED — it will fail)
test('capitalizes first letter', () => {
expect(capitalize('hello')).toBe('Hello');
});
// Step 2: Write the function to make it pass (GREEN)
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// Step 3: Refactor if needed (the code looks clean, so we're done)
Benefits of TDD: it forces you to think about what your code should do before writing it, leads to cleaner design, and gives you confidence when refactoring later.
Note: TDD is not required on every project — but understanding it makes you a better developer and is a common interview topic.- Testing means verifying that your code does what you expect — it saves time, prevents bugs, and builds confidence
- Three main types: Unit (one function), Integration (multiple pieces together), End-to-End (full user flow)
- Unit tests are the best starting point — fast, isolated, and easy to write
- The Arrange → Act → Assert pattern is the core structure of any unit test
- Jest is the most popular JavaScript testing library — easy to read and fast to run
- test() defines a test case, expect() checks a value, .toBe() checks strict equality
- describe() groups related tests together for better organisation
- TDD (Test-Driven Development) means writing the test first, then the code — Red → Green → Refactor
- Good tests check the happy path, edge cases, and invalid inputs
- What is software testing and why is it important?
- What is the difference between unit testing, integration testing, and end-to-end testing?
- What is the Arrange → Act → Assert pattern in unit testing?
- What is Jest and what is it used for?
- What is the difference between toBe() and toEqual() in Jest?
- What does the describe() function do in Jest?
- What is Test-Driven Development (TDD)?
- What does the Red → Green → Refactor cycle mean in TDD?
- Why should you test edge cases in addition to the happy path?
- Can you write a test without any library? How?
- What makes a good unit test?