Testing strategieën in Node.js: praktische gids

Leer welke testing strategieën in Node.js werken. Van unit tests met Jest tot integration en end-to-end tests, met praktische voorbeelden.

18 juli 20266 min leestijdDoor We Develop Communication

Een robuuste testing strategie in Node.js bepaalt of jouw applicatie met vertrouwen naar productie gaat of dat elke deploy een gok is. Zonder tests vertrouw je op handmatig klikken en hoop. Met de juiste teststrategie vang je regressies vroeg, documenteer je gedrag en kun je veilig refactoren.

In deze gids lopen we de belangrijkste testing strategieën door, van unit tests tot end-to-end, en laten we met praktische code zien hoe je ze inzet in Node.js projecten.

De testing pyramid kort uitgelegd

De testing pyramid is een klassiek model dat helpt om je testinspanning in verhouding te houden. Onderaan staan veel snelle, goedkope tests. Bovenaan weinig, maar duurdere tests.

  • Unit tests, veel, snel, isoleren één functie of klasse
  • Integration tests, minder, testen samenwerking tussen componenten
  • End-to-end tests, weinig, testen de volledige flow door het systeem

Deze verdeling werkt omdat unit tests in milliseconden draaien, terwijl een e2e-test al snel tientallen seconden kost. Een pyramid met veel e2e's onderin maakt je CI-pipeline traag en broos.

Unit tests: de basis van je strategie

Unit tests controleren één functie of module in isolatie. Dependencies worden gemockt of vervangen zodat je écht alleen de logica test.

Met Jest ziet dat er als volgt uit:

// math.js
export function calculateTax(amount, rate) {
  if (amount < 0) throw new Error('Amount cannot be negative');
  return Number((amount * rate).toFixed(2));
}

// math.test.js
import { calculateTax } from './math.js';

describe('calculateTax', () => {
  it('berekent btw correct', () => {
    expect(calculateTax(100, 0.21)).toBe(21);
  });

  it('rondt af op twee decimalen', () => {
    expect(calculateTax(33.33, 0.21)).toBe(7);
  });

  it('gooit een error bij negatief bedrag', () => {
    expect(() => calculateTax(-10, 0.21)).toThrow();
  });
});

Goede unit tests zijn snel, deterministisch en richten zich op één ding per test. Gebruik duidelijke namen zodat je test-output leest als een spec.

Pure functies vs. code met side effects

Pure functies zijn het makkelijkst te testen: gelijke input geeft gelijke output. Voor code met side effects (database, filesystem, HTTP) moet je mocks inzetten, of een andere teststrategie kiezen.

Gebruik je TypeScript? Combineer unit tests met Zod-validatie om zowel je types als runtime-gedrag af te dekken.

Integration tests: waar modules samenwerken

Integration tests verifiëren dat meerdere onderdelen samen werken zoals bedoeld. Denk aan een route die een service aanroept, die weer een database raakt.

import request from 'supertest';
import { app } from './app.js';
import { db } from './db.js';

beforeAll(async () => {
  await db.migrate.latest();
});

afterAll(async () => {
  await db.destroy();
});

beforeEach(async () => {
  await db('users').truncate();
});

describe('POST /users', () => {
  it('maakt een nieuwe user aan', async () => {
    const response = await request(app)
      .post('/users')
      .send({ email: '[email protected]', name: 'Sander' });

    expect(response.status).toBe(201);
    expect(response.body).toMatchObject({ email: '[email protected]' });

    const users = await db('users').select('*');
    expect(users).toHaveLength(1);
  });
});

Bij integration tests wil je meestal geen mocks voor je database. Een echte test-database (vaak in Docker of via Testcontainers) vangt migratieproblemen en query-fouten die mocks verbergen.

Lees ook onze gids over werken met databases in Node.js voor connection pooling en transacties in testomgevingen.

End-to-end tests: het grote geheel

End-to-end (E2E) tests draaien tegen een volledig opgestarte applicatie. Ze simuleren echte gebruikers of consumers en raken alle lagen, frontend, backend, database, queues.

Voor HTTP-API's gebruik je Supertest of Playwright's API-testing. Voor WebSockets en realtime apps kun je een client-library inzetten om een echte connectie op te zetten.

import { test, expect } from '@playwright/test';

test('gebruiker kan inloggen en dashboard zien', async ({ page }) => {
  await page.goto('http://localhost:3000/login');
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('input[name="password"]', 'secret123');
  await page.click('button[type="submit"]');

  await expect(page).toHaveURL('/dashboard');
  await expect(page.locator('h1')).toContainText('Welkom');
});

Houd het aantal E2E-tests beperkt tot de belangrijkste user journeys: inloggen, checkout, een kernfeature. Alles daarbuiten hoort meestal op een lager niveau thuis.

Test runners en frameworks

In Node.js heb je meerdere populaire test runners:

  • Jest, marktleider, veel plugins, snapshot testing, ingebouwde mocking
  • Vitest, snel, modern, Vite-compatible, bijna identieke API als Jest
  • node:test, sinds Node 20 stabiel, geen dependencies nodig
  • Mocha + Chai, klassiek, flexibel, vereist meer configuratie

Een minimale test met node:test ziet er zo uit:

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { calculateTax } from './math.js';

test('calculateTax berekent btw correct', () => {
  assert.equal(calculateTax(100, 0.21), 21);
});

Draaien doe je met node --test. Voor kleine projecten of libraries zonder complexe requirements is dit een prima keuze zonder extra dependencies.

Mocks, stubs en test doubles

Test doubles vervangen echte dependencies in je tests. Ze zijn cruciaal voor snelheid en isolatie, maar overgebruik leidt tot tests die niks zinnigs testen.

Wanneer wel mocken

  • Externe API-calls (third-party services, betalingen)
  • Trage dependencies (e-mails versturen, PDF-generatie)
  • Niet-deterministisch gedrag (Date.now(), random)

Wanneer niet mocken

  • Je eigen code die je wilt testen
  • De database in integration tests
  • Pure functies

Een voorbeeld met Jest:

import { sendWelcomeEmail } from './email.js';
import { createUser } from './users.js';

jest.mock('./email.js');

it('stuurt welkomstmail na registratie', async () => {
  await createUser({ email: '[email protected]' });

  expect(sendWelcomeEmail).toHaveBeenCalledWith('[email protected]');
});

Bekijk ook onze artikelen over error handling op schaal en authentication met JWT en sessions om te zien hoe je deze onderdelen testbaar ontwerpt.

Async code testen

Omdat Node.js async-first is, draaien de meeste tests om promises en async/await. Vergeet nooit te await-en of te return-en:

it('haalt data op', async () => {
  const data = await fetchUser(1);
  expect(data.id).toBe(1);
});

Voor streams of background jobs wil je vaak wachten op een event voordat je assert doet. Gebruik dan new Promise of tools als p-event om dit elegant te doen.

Meer achtergrond? Lees onze gids over async programming met promises en async/await.

Coverage en rapportage

Coverage-tools meten welke regels door je tests geraakt worden. Jest, Vitest en c8 produceren allemaal rapporten in formaten als text, html en lcov.

jest --coverage

Stel een minimum in via configuratie:

{
  "coverageThreshold": {
    "global": {
      "lines": 80,
      "branches": 75,
      "functions": 80
    }
  }
}

Let op: hoge coverage is geen garantie voor kwaliteit. Een test die alle regels raakt maar niks assert, telt mee in het rapport maar vangt geen bugs.

Tests in je CI/CD pipeline

Tests horen bij elke push en pull request te draaien. Splits je suite op zodat snelle unit tests als eerste falen, dat geeft developers sneller feedback.

# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm run test:unit
  integration:
    runs-on: ubuntu-latest
    needs: unit
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_PASSWORD: test }
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test:integration

Zie de Node.js docs over testing voor de officiële testing API.

Veelgemaakte fouten

  • Te veel mocks: als elke dependency gemockt is, test je alleen nog je mock-setup.
  • Tests die van elkaar afhangen: elke test moet onafhankelijk kunnen draaien, in willekeurige volgorde.
  • Flaky tests: tests die soms falen door timing of shared state. Fix direct, negeren erodeert het vertrouwen in je suite.
  • Geen teardown: database rijen of open connecties lekken naar volgende tests.
  • Snapshots zonder review: blind accepteren van snapshot-updates maakt ze waardeloos.

Praktische strategie voor jouw project

Begin klein en bouw uit:

  1. Schrijf unit tests voor pure business logic en utilities
  2. Voeg integration tests toe voor kritieke endpoints
  3. Dek je belangrijkste user journeys met 3–5 E2E tests
  4. Zet coverage-drempels in CI
  5. Review flaky tests wekelijks

Voor grotere applicaties helpt een goede projectstructuur om tests naast hun source-files te plaatsen, wat onderhoud een stuk makkelijker maakt.

Veelgestelde vragen

Welke testing strategieën zijn er in Node.js?

De drie belangrijkste strategieën zijn unit tests (losse functies), integration tests (samenwerking tussen modules of services) en end-to-end tests (complete gebruikersflow). Samen vormen ze de bekende testing pyramid.

Welke test runner kan ik het beste gebruiken?

Jest is veruit de populairste keuze met een complete toolset. Vitest is sneller en moderner, en de ingebouwde node:test module werkt zonder dependencies. Kies op basis van snelheid, ecosysteem en teamvoorkeur.

Hoeveel test coverage moet ik nastreven?

80% line coverage is een veelgebruikte richtlijn, maar 100% coverage betekent niet dat je code bugvrij is. Focus op kritieke business logic en randgevallen in plaats van blind op een percentage te sturen.

Moet ik mijn database mocken in tests?

Voor unit tests wel, voor integration tests meestal niet. Gebruik een test-database of tools zoals Testcontainers om realistische scenario's te testen. Pure mocks missen vaak problemen rondom migraties en query-gedrag.

Wat is het verschil tussen een mock, stub en spy?

Een stub vervangt een functie met een vaste response. Een spy registreert hoe een functie wordt aangeroepen. Een mock combineert beide: hij vervangt gedrag én verifieert het gebruik ervan.

Veelgestelde vragen

Klaar om digitaal te groeien?

Wij helpen Nederlandse bedrijven met webtechnologie en SEO-strategieën die écht werken. Neem vrijblijvend contact op.