iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…