Testing Strategy — Practical
Testing — Practical (per stack)
Section titled “Testing — Practical (per stack)”Node / TypeScript — Vitest / Jest
Section titled “Node / TypeScript — Vitest / Jest”import { describe, it, expect } from 'vitest';import { add } from './math';
describe('add', () => { it('returns sum of two ints', () => { expect(add(2, 3)).toBe(5); });
it.each([ [0, 0, 0], [-1, 1, 0], [1.5, 2.5, 4], ])('add(%d, %d) === %d', (a, b, want) => { expect(add(a, b)).toBe(want); });});Async tests with timeout & cleanup
Section titled “Async tests with timeout & cleanup”it('publishes event after save', async () => { const spy = vi.fn(); bus.on('UserCreated', spy); await service.createUser({ email: 'a@b' }); await vi.waitFor(() => expect(spy).toHaveBeenCalled());});
afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks();});Integration test with Testcontainers (Postgres)
Section titled “Integration test with Testcontainers (Postgres)”import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';import { Pool } from 'pg';
let pg: StartedPostgreSqlContainer;let pool: Pool;
beforeAll(async () => { pg = await new PostgreSqlContainer('postgres:16').start(); pool = new Pool({ connectionString: pg.getConnectionUri() }); await pool.query(`CREATE TABLE users(id SERIAL PRIMARY KEY, email TEXT UNIQUE)`);});afterAll(async () => { await pool.end(); await pg.stop(); });
it('inserts unique users', async () => { await pool.query(`INSERT INTO users(email) VALUES ('a@b')`); await expect(pool.query(`INSERT INTO users(email) VALUES ('a@b')`)) .rejects.toThrow(/duplicate/);});Pact (consumer side)
Section titled “Pact (consumer side)”import { Pact } from '@pact-foundation/pact';
const provider = new Pact({ consumer: 'web', provider: 'users-api' });
beforeAll(() => provider.setup());afterAll(() => provider.finalize());
it('GET /users/1 returns user', async () => { await provider.addInteraction({ state: 'user 1 exists', uponReceiving: 'a request for user 1', withRequest: { method: 'GET', path: '/users/1' }, willRespondWith: { status: 200, body: { id: '1', email: 'a@b' } }, }); const r = await fetch(`${provider.mockService.baseUrl}/users/1`); expect(await r.json()).toEqual({ id: '1', email: 'a@b' });});Property-based with fast-check
Section titled “Property-based with fast-check”import fc from 'fast-check';
it('reverse twice = identity', () => { fc.assert(fc.property(fc.array(fc.integer()), arr => { expect(reverse(reverse(arr))).toEqual(arr); }));});HTTP API (supertest)
Section titled “HTTP API (supertest)”import request from 'supertest';import { app } from '../app';
it('POST /users returns 201', async () => { const r = await request(app).post('/users').send({ email: 'a@b.com' }); expect(r.status).toBe(201); expect(r.body).toMatchObject({ email: 'a@b.com' });});Python — pytest
Section titled “Python — pytest”import pytest
@pytest.mark.parametrize("a,b,want", [(0,0,0), (-1,1,0), (2,3,5)])def test_add(a, b, want): assert add(a, b) == want
@pytest.fixturedef db(): conn = create_test_db() yield conn conn.rollback() conn.close()
def test_insert(db): db.execute("INSERT INTO users(email) VALUES ('a@b')") assert db.fetchone("SELECT count(*) FROM users")[0] == 1Mock external HTTP (msw / responses)
Section titled “Mock external HTTP (msw / responses)”import { setupServer } from 'msw/node';import { http, HttpResponse } from 'msw';
const server = setupServer( http.get('https://api.x.com/users/1', () => HttpResponse.json({ id: 1 })));beforeAll(() => server.listen());afterAll(() => server.close());Load test (k6)
Section titled “Load test (k6)”import http from 'k6/http';import { check, sleep } from 'k6';
export const options = { stages: [ { duration: '30s', target: 50 }, { duration: '2m', target: 200 }, { duration: '30s', target: 0 }, ], thresholds: { http_req_duration: ['p(95)<500', 'p(99)<1000'], http_req_failed: ['rate<0.01'], },};
export default function () { const r = http.get('https://api/items'); check(r, { 'status 200': (x) => x.status === 200 }); sleep(1);}E2E (Playwright sketch)
Section titled “E2E (Playwright sketch)”import { test, expect } from '@playwright/test';
test('user can sign up and see dashboard', async ({ page }) => { await page.goto('/signup'); await page.fill('[name=email]', 'a@b.com'); await page.fill('[name=password]', 'pw'); await page.click('text=Sign up'); await expect(page).toHaveURL('/dashboard');});CI structure (.github/workflows)
Section titled “CI structure (.github/workflows)”jobs: ci: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: { POSTGRES_PASSWORD: pw } ports: [ '5432:5432' ] options: --health-cmd "pg_isready -U postgres" --health-interval 10s steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 20, cache: pnpm } - run: pnpm i --frozen-lockfile - run: pnpm lint - run: pnpm test:unit - run: pnpm test:integration - run: pnpm buildTooling cheat sheet
Section titled “Tooling cheat sheet”- Node: Vitest / Jest / Mocha + chai. Supertest. Testcontainers. msw. fast-check. Pact.
- Python: pytest + pytest-asyncio + pytest-postgresql. responses/respx. hypothesis.
- Go: stdlib
testing+testify. Testcontainers-go. gomock. quick (PBT). - Java/Kotlin: JUnit5 + AssertJ + Mockito. Testcontainers. RestAssured. Pact-JVM.
- Load: k6, Locust, wrk, vegeta.
- E2E: Playwright, Cypress.
- Mutation: Stryker (JS), mutmut (Python), PIT (Java).