Kodokon kodokon.com

Тестируем API: node:test, fetch и тестовая база данных

Тестируй маршруты от начала до конца с помощью node:test и fetch на эфемерном порту, где у каждого теста своя база SQLite в памяти.

9 мин · 3 вопросов

Открыть этот урок в Kodokon

В Node 20 есть всё необходимое: node:test в роли раннера, node:assert/strict для утверждений, глобальный fetch как HTTP-клиент. Ноль зависимостей на тесты. И вот здесь окупается архитектура из первого урока: createApp({ db }) принимает внедрённую базу данных, так что каждый тест может поднять целое изолированное API за несколько миллисекунд.

BASH
node --test
node --test --watch
Раннер сам находит файлы *.test.js в проекте.

Чтобы протестировать маршруты от начала до конца, не нужно подменять Express заглушками: подними настоящий сервер на порту 0 - система назначит свободный порт - и обратись к нему через fetch. Ты проверяешь ровно то, что увидит настоящий клиент: маршрутизацию, middleware, сериализацию, коды состояния. Это дух supertest, но без зависимости.

JAVASCRIPT
import { once } from 'node:events';

export async function startTestServer(app) {
  const server = app.listen(0);
  await once(server, 'listening');
  const { port } = server.address();
  const url = `http://127.0.0.1:${port}`;
  const close = () =>
    new Promise((resolve) => {
      server.close(resolve);
    });
  return { url, close };
}
helpers.js: настоящий сервер на порту, выданном системой.

Тестовая база данных: SQLite в памяти (:memory:). Каждый тест строит собственную свежую пустую базу через createDb, которая применяет схему, а на выходе выбрасывает её - никаких данных, общих между тестами, никакого скрипта очистки. t.after гарантирует, что сервер закроется, даже когда утверждение провалится.

JAVASCRIPT
import test from 'node:test';
import assert from 'node:assert/strict';
import { createDb } from '../src/db/connection.js';
import { createApp } from '../src/app.js';
import { startTestServer } from './helpers.js';

test('POST /api/users creates a user', async (t) => {
  const db = createDb(':memory:');
  const app = createApp({ db });
  const server = await startTestServer(app);
  t.after(() => server.close());

  const res = await fetch(`${server.url}/api/users`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      email: 'ada@example.com',
      name: 'Ada',
    }),
  });

  assert.equal(res.status, 201);
  const body = await res.json();
  assert.equal(body.email, 'ada@example.com');
});
Тест от начала до конца: настоящий HTTP-стек, одноразовая база.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Почему тестовый сервер стоит запускать через app.listen(0)?
    • Порт 0 отключает сеть и ускоряет тесты
    • Система назначает свободный порт: параллельные тесты никогда не дерутся за один и тот же порт
    • Это единственный порт, разрешённый вне продакшена
    • Порт 0 переводит Express в тестовый режим
  2. В чём решающее преимущество базы :memory:, создаваемой в каждом тесте?
    • Она сохраняет данные между запусками для сравнения
    • Каждый тест стартует с чистого изолированного состояния, без скрипта очистки и без взаимных помех
    • Она отключает ограничения SQL, чтобы упростить вставки
  3. Почему сервер закрывают в t.after, а не в конце тела теста?
    • t.after выполняется, даже если утверждение провалилось, что избавляет от осиротевших серверов, мешающих процессу завершиться
    • t.after выполняется до теста, что подготавливает сервер
    • Это чисто вопрос стиля, поведение одинаковое