选 Vitest 还是 Jest

Vitest Jest
速度 (基于 Vite)
ESM 支持 原生 需要配置
TypeScript 一等公民 需配 babel/ts-jest
API 几乎兼容 Jest 老牌
推荐 新项目 老项目维护

下面用 Vitest 举例。Jest 用户大多数 API 直接通用。

npm install -D vitest

package.json:

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "coverage": "vitest run --coverage"
  }
}

第一个测试

// math.js
export function add(a, b) {
    return a + b;
}

// math.test.js
import { describe, it, expect } from 'vitest';
import { add } from './math.js';

describe('add', () => {
    it('adds two numbers', () => {
        expect(add(1, 2)).toBe(3);
    });

    it('handles negatives', () => {
        expect(add(-1, 1)).toBe(0);
    });
});
npm test

常用 expect 匹配

expect(42).toBe(42);                          // === 严格相等
expect({a: 1}).toEqual({a: 1});               // 深比较
expect([1,2,3]).toContain(2);
expect('hello').toMatch(/ello/);
expect(arr).toHaveLength(3);
expect(obj).toHaveProperty('name');
expect(fn).toThrow();
expect(fn).toThrow('specific message');
expect(promise).resolves.toBe('ok');
expect(promise).rejects.toThrow();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeTruthy();
expect(value).toBeGreaterThan(10);

异步测试

it('fetches user', async () => {
    const user = await getUser(42);
    expect(user.name).toBe('Alice');
});

前后钩子

import { beforeAll, beforeEach, afterAll, afterEach } from 'vitest';

describe('users', () => {
    beforeAll(async () => {
        // 一次:测试开始前
        await db.connect();
    });

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

    beforeEach(async () => {
        // 每个测试前
        await db.users.deleteMany({});
    });

    afterEach(() => {});

    it(...);
});

Mock

简单函数 mock

import { vi } from 'vitest';

const fn = vi.fn(() => 42);
fn();
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith(/* args */);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveReturnedWith(42);

Mock 模块

import { vi } from 'vitest';
import { sendEmail } from './email.js';

vi.mock('./email.js', () => ({
    sendEmail: vi.fn().mockResolvedValue({ ok: true }),
}));

it('sends email on signup', async () => {
    await signup({ email: 'a@x' });
    expect(sendEmail).toHaveBeenCalledWith('a@x', expect.stringContaining('welcome'));
});

Mock 时间

vi.useFakeTimers();
const fn = vi.fn();
setTimeout(fn, 1000);
vi.advanceTimersByTime(1000);
expect(fn).toHaveBeenCalled();
vi.useRealTimers();

集成测试 HTTP API(supertest)

npm install -D supertest
import request from 'supertest';
import { app } from './app.js';

describe('GET /users', () => {
    it('returns list', async () => {
        const res = await request(app)
            .get('/users')
            .expect(200);
        expect(res.body).toBeInstanceOf(Array);
    });

    it('POST creates user', async () => {
        const res = await request(app)
            .post('/users')
            .send({ name: 'Alice', email: 'a@x' })
            .expect(201);
        expect(res.body.id).toBeDefined();
    });
});

supertest 自动启动 / 关闭 app——不用 listen 真端口

测试数据库

两种姿势:

方式 A · 内存 DB(快)

SQLite in-memory / Postgres in Docker / mongodb-memory-server。

// 全局 setup
beforeAll(async () => {
    await db.connect(':memory:');
    await db.migrate();
});

方式 B · 真 DB + 事务回滚

beforeEach(async () => {
    await db.beginTransaction();
});

afterEach(async () => {
    await db.rollback();        // 每个测试自动清理
});

覆盖率

npm install -D @vitest/coverage-v8

# vitest.config.js 加 coverage 配置
npm run coverage

输出 coverage/index.html 看具体哪些行没覆盖。

测试金字塔

        E2E 测试 (Playwright / Cypress)  ← 少
       /
   集成测试(supertest)             ← 中
  /
单元测试(Vitest)                    ← 多

单元测试占大头——快 + 易写 + 易维护。

  • 测试不要依赖外部网络 / 真数据库——慢 + 不可靠
  • Mock 太多 → 测试和实现耦合 → 重构难
  • 异步测试忘 await = 测试通过但断言没生效
  • 全局状态测试间共享 → 互相影响(用 beforeEach 清理)

下一篇:TypeScript 接入。