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 文件。

要端到端地测试路由,无需去 mock Express:在端口 0 上挂载一个真实的服务器(系统会分配一个空闲端口),然后用 fetch 去打它。你演练的正是真实客户端将会看到的一切:路由、中间件、序列化、状态码。这就是 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 在测试之前运行,用于准备服务器
    • 这纯粹是风格问题,行为完全相同