Expert · Performance, security and testing · topic 30 of 34 · 30 min
Testing a full-stack app
Explanation
Why write tests?
Every time you change the Notes app, something old can break: a fix in search can break pinning. Clicking through every screen by hand takes 20 minutes and you will skip it when you are tired. A test is code that checks your code, and it runs in seconds, every time.
| Level | What it checks | Speed | Notes app example | Common tool |
|---|---|---|---|---|
| Unit | One function alone | Milliseconds | validateNote rejects an empty title | Vitest, Jest |
| Component | One UI part with fake data | Fast | NoteForm shows "Title is required" | Testing Library |
| Integration | API handler + real test database | Seconds | POST /api/notes saves a row | Vitest + Supertest |
| End-to-end (E2E) | The whole app in a real browser | Slow | Sign up, add a note, see it in the list | Playwright |
Unit tests: the base of the pyramid
import { test, expect } from 'vitest';
import { validateNote } from './validate-note.js';
test('rejects a title that is only spaces', () => {
expect(validateNote({ title: ' ' }).ok).toBe(false);
});
test('accepts a 100-character title but not 101', () => {
expect(validateNote({ title: 'a'.repeat(100) }).ok).toBe(true);
expect(validateNote({ title: 'a'.repeat(101) }).ok).toBe(false);
});Integration tests: API plus a test database
import request from 'supertest';
import { createApp } from '../src/app.js';
import { createTestDb } from './helpers.js';
test('a user cannot delete another user\'s note', async () => {
const db = createTestDb(); // fresh SQLite file or schema for each test
const app = createApp(db);
const asha = await signupAndLogin(app, 'asha@example.com');
const ravi = await signupAndLogin(app, 'ravi@example.com');
const created = await asha.post('/api/notes').send({ title: 'Rent' });
const res = await ravi.delete('/api/notes/' + created.body.id);
expect(res.status).toBe(404);
});Component and end-to-end tests
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { test, expect, vi } from 'vitest';
import { NoteForm } from './NoteForm';
test('shows an error and does not save an empty note', async () => {
const onSave = vi.fn();
render(<NoteForm onSave={onSave} />);
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
expect(screen.getByText('Title is required')).toBeTruthy();
expect(onSave).not.toHaveBeenCalled();
});import { test, expect } from '@playwright/test';
test('user can log in and create a note', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.getByLabel('Email').fill('test-user@example.com');
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD ?? '');
await page.getByRole('button', { name: 'Log in' }).click();
await page.getByRole('button', { name: 'New note' }).click();
await page.getByLabel('Title').fill('Buy milk');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Buy milk')).toBeVisible();
});What to test first
- Money: prices, totals, discounts, refunds. A wrong number here costs real rupees.
- Auth and permissions: signup, login, logout, and "user A cannot read, edit or delete user B's notes".
- Data-loss paths: delete, edit, import and migrations. Losing a user's notes is the worst bug a notes app can have.
- Every bug you fix: write a test that fails because of the bug, then fix it. This is a regression test, and the bug can never quietly come back.
Tests in CI, and flaky tests
# .github/workflows/test.yml
name: test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm testA flaky test passes sometimes and fails sometimes with no code change. Common causes are real timers, tests that depend on each other's data, the current date, and waiting for a fixed time instead of waiting for something to appear. Fix the cause: give each test its own data, pass a fake clock, and use waits like Playwright's expect(...).toBeVisible() that retry until the element shows.
Watch the lesson
Testing a full-stack app · 1/6
Every change can break something old. Tests check your app for you, in seconds, every time.
Prefer a video by a teacher? Find free videos on “testing pyramid unit integration end to end tests javascript vitest playwright” on YouTube ↗
In real life: The fees bug that tests would have caught
A coaching institute's fee app gave a 10% sibling discount. A developer changed the discount code to add a festival offer, and both discounts started stacking, so some families were charged 19% less. Nobody noticed for a month. After that, the team added unit tests for every discount rule ("sibling only: 10%", "sibling plus festival: the bigger one, not both"), an integration test for the invoice API and one end-to-end test that pays a fee in the payment gateway's test mode. The tests run in CI on every push, so a change that breaks a rule cannot be merged.
Practice (0/3 done)
Make the tests pass. Write searchNotes(notes, query). Each note is { id, title, body, pinned }. Return the notes whose title or body contains the query, ignoring upper/lower case and spaces at the ends of the query. An empty query returns all notes. Pinned notes come first; otherwise keep the original order. Do not change the input array.
An integration-style test runs create -> list -> delete against an in-memory API. Write createNotesApi() returning { handle(req) }. req is { method, path, userId, body }. Rules: no userId -> 401. POST /api/notes: trimmed title must be 1-100 characters, else 400; otherwise save { id, userId, title, body, pinned: false } (ids start at 1, body defaults to "") and return { status: 201, body: note }. GET /api/notes -> 200 with only this user's notes. DELETE /api/notes/:id -> 204 if it is this user's note, else 404. Anything else -> 404.
Write a test plan for your Notes app: which tests you will write at each level, and in which order. Then write and run at least the first three.
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.