node:test & vitest
Node testing in 2026: node:test (built-in), Vitest, Jest. Patterns for unit, integration, e2e.
Node — testing
EXAMPLE
# ===== Built-in node:test =====
// Available since Node 18 LTS.
// math.test.js
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
describe('math', () => {
test('adds', () => {
assert.equal(1 + 1, 2);
});
test('throws on bad input', () => {
assert.throws(() => { throw new Error('bad'); }, /bad/);
});
});
// Run: node --test
// Or: node --test --test-reporter=spec
# ===== Vitest (fast, modern) =====
npm install -D vitest
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: { environment: 'node', coverage: { reporter: ['text', 'lcov'] } },
});
// example.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
describe('user service', () => {
it('creates a user', async () => {
const created = await service.create({ name: 'Alex' });
expect(created.id).toBeDefined();
});
});
# ===== Mocking =====
// Vitest:
const fetchMock = vi.fn().mockResolvedValue({ json: async () => [{ id: 1 }] });
vi.stubGlobal('fetch', fetchMock);
// node:test (Node 19.1+):
import { mock } from 'node:test';
const fn = mock.fn(() => 42);
console.log(fn()); // 42
console.log(fn.mock.callCount()); // 1
# ===== Integration tests with Testcontainers =====
import { GenericContainer } from 'testcontainers';
let container;
beforeAll(async () => {
container = await new GenericContainer('postgres:16')
.withEnvironment({ POSTGRES_PASSWORD: 'pw' })
.withExposedPorts(5432)
.start();
process.env.DATABASE_URL = \`postgres://postgres:pw@${container.getHost()}:${container.getMappedPort(5432)}/postgres\`;
});
afterAll(async () => {
await container.stop();
});
# ===== HTTP tests with supertest =====
import request from 'supertest';
import { app } from './app';
it('GET /healthz', async () => {
const r = await request(app).get('/healthz').expect(200);
expect(r.body.ok).toBe(true);
});
# ===== E2E with Playwright =====
npm install -D @playwright/test
npx playwright install
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test('user can sign in', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.fill('[name=email]', 'a@x.io');
await page.fill('[name=password]', '...');
await page.click('button[type=submit]');
await expect(page).toHaveURL(/dashboard/);
});
# ===== Test types =====
# Unit: pure functions; fast; many
# Integration: hits DB / Redis / external services via Testcontainers
# E2E: full UI via Playwright; expensive; few
# Test pyramid: many unit, fewer integration, very few e2e.
# ===== Patterns =====
# - Vitest for new projects; node:test when you want zero deps
# - Testcontainers for integration; no mock DBs in 2026
# - Playwright for e2e; runs in CI on every PR
# - Coverage as guardrail, not target
# - Snapshot tests sparingly
# ===== Pitfalls =====
# - Tests that hit real services -> flake
# - Mocking the SUT instead of its dependencies
# - 100% coverage chase
# - No CI integration -> tests rot
Why it matters
Node testing: node:test for zero-dep, Vitest for modern apps, Testcontainers for integration, Playwright for E2E. Pyramid: many unit, fewer integration, few E2E. Coverage as guardrail; never the goal.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { test, expect } from 'vitest';
import { add } from './math.js';
test('add', () => expect(add(2, 3)).toBe(5));
Try it Yourself »
Discussion
Loading…