Expert · Fast, accessible, findable and safe: quality for real sites · topic 50 of 64 · 35 min
Testing basics: unit, integration and end-to-end
Explanation
Why test?
Without tests, you check your site by hand: open it, click around, and hope nothing broke. That is slow, and you forget steps. A test suite checks hundreds of things in seconds. It catches bugs before users do, lets you change code without fear (refactoring), and documents what the code is supposed to do.
The test pyramid
| Level | What it checks | Speed | How many |
|---|---|---|---|
| Unit | One function or component on its own | Milliseconds | Many |
| Integration | Several parts together, for example a form with its validation and a faked API | Fast | Some |
| End-to-end (E2E) | The whole app in a real browser, like a user: open the page, type, click, see the result | Seconds each | A few, for the most important journeys such as login and checkout |
A unit test: Arrange, Act, Assert
// cart.js
export function cartTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
// cart.test.js
import { describe, it, expect } from 'vitest';
import { cartTotal } from './cart.js';
describe('cartTotal', () => {
it('adds price times quantity for every item', () => {
// Arrange: set up the data
const items = [{ price: 40, qty: 2 }, { price: 25, qty: 1 }];
// Act: call the code
const total = cartTotal(items);
// Assert: check the result
expect(total).toBe(105);
});
it('is 0 for an empty cart', () => {
expect(cartTotal([])).toBe(0);
});
});- toBe checks exact equality (like ===), good for numbers, strings and booleans.
- toEqual compares the contents of objects and arrays.
- toThrow checks that a function throws an error.
- Jest uses almost the same describe, it and expect syntax, so the skill carries over.
Mocking: faking the network
import { it, expect, vi } from 'vitest';
import { loadUser } from './api.js';
it('returns the user name from the API', async () => {
const fakeFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ name: 'Asha' }),
});
vi.stubGlobal('fetch', fakeFetch);
const user = await loadUser(7);
expect(fakeFetch).toHaveBeenCalledWith('/api/users/7');
expect(user.name).toBe('Asha');
});Testing what the user sees
Testing Library (for React, Vue, plain DOM and more) finds elements the way a user does: by role, label and visible text, not by CSS classes. A query like screen.getByRole("button", { name: "Pay" }) works only if the button really has that accessible name, so these tests also push you toward accessible HTML. Playwright goes one step further and drives real browsers.
// tests/order.spec.js
import { test, expect } from '@playwright/test';
test('user can add a dosa and see the total', async ({ page }) => {
await page.goto('http://localhost:5173/');
await page.getByRole('button', { name: 'Add Masala Dosa' }).click();
await expect(page.getByText('Total: ₹80')).toBeVisible();
});Run tests on every push (CI)
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
- run: npm test- A good test checks one behaviour and has a name that says it: "gives free delivery at 199 or more".
- It covers edge cases: empty lists, 0, the exact limit, very big numbers and wrong input.
- It is independent: it does not depend on other tests or on the order they run in.
- It is reliable: no real network, no real clock, no random values unless you control them.
- It fails when the code is wrong. A test that can never fail is worse than no test.
Watch the lesson
Testing basics: unit, integration and end-to-end · 1/7
A test is code that checks your code. It runs in seconds, every time you change something.
Prefer a video by a teacher? Find free videos on “vitest tutorial unit testing javascript playwright for beginners” on YouTube ↗
In real life: A billing app that stopped giving wrong discounts
A shop billing app has a "buy 2, get 1 free" rule. A developer changes the code and, by mistake, gives the free item only when the quantity is more than 3. Customers buying exactly 3 now pay for all of them. The team adds unit tests for quantities 0, 2, 3, 4 and 6. The test for 3 fails at once. Because GitHub Actions runs the tests on every pull request and the main branch only accepts green pull requests, the next mistake like this never reaches the shop.
Practice (0/3 done)
Build the heart of a test library yourself. Write expectEqual(actual, expected). If JSON.stringify(actual) and JSON.stringify(expected) are the same, do nothing. Otherwise throw a new Error with the message "Expected <expected> but got <actual>", using the JSON text of each. Examples: expectEqual(2 + 2, 5) throws an Error with the message "Expected 5 but got 4"; expectEqual([1, 2], [1, 2]) does nothing.
A tester's job is to find the case that breaks the code. The leap year rule: a year is a leap year if it is divisible by 4, except years divisible by 100, unless they are also divisible by 400. So 2024 and 2000 are leap years, but 1900 and 2023 are not. A teammate wrote isLeapYear(year) as "return year % 4 === 0", which is wrong. Write an array called cases with at least 4 test cases, each { year, expected } where expected is true or false. Every case must be right for the real rule, and at least one case must make the buggy version fail. The checker runs your cases against both versions.
Add real tests to one of your projects. Install Vitest (npm install -D vitest), add "test": "vitest run" to the scripts in package.json, write tests for two pure functions, then add a GitHub Actions workflow that runs npm test on every push.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.