close

Test

test 用于定义一个测试用例,支持链式调用和 fixture 扩展。

别名:it

test

  • 类型: (name: string, fn: (testContext: TestContext) => void | Promise<void>, timeout?: number) => void

定义一个测试用例。

import { expect, test } from '@rstest/core';

test('should add two numbers correctly', () => {
  expect(1 + 1).toBe(2);
  expect(1 + 2).toBe(3);
});

test.only

只运行测试文件中的某些测试。

test.only('run only this test', () => {
  // ...
});

test.skip

跳过某些测试。

test.skip('skip this test', () => {
  // ...
});

test.todo

将某些测试标记为待办。

test.todo('should implement this test');

test.each

  • 类型: test.each(cases: ReadonlyArray<T>)(name: string, fn: (param: T) => void | Promise<void>, timeout?: number) => void

对提供的数组中的每一项运行相同的测试逻辑。

test.each([
  { a: 1, b: 2, sum: 3 },
  { a: 2, b: 2, sum: 4 },
])('adds $a + $b', ({ a, b, sum }) => {
  expect(a + b).toBe(sum);
});

你也可以使用标签模板字面量的表格语法,使参数化测试更具可读性:

test.each`
  a    | b    | expected
  ${1} | ${2} | ${3}
  ${2} | ${3} | ${5}
`('$a + $b = $expected', ({ a, b, expected }) => {
  expect(a + b).toBe(expected);
});

第一行定义参数名(列标题),后续每行通过模板表达式(${...})提供值,列之间用 | 分隔。

由于表格中的值默认是无类型的,你可以通过显式泛型参数来获得类型支持:

test.each<{ a: number; b: number; expected: number }>`
  a    | b    | expected
  ${1} | ${2} | ${3}
  ${2} | ${3} | ${5}
`('$a + $b = $expected', ({ a, b, expected }) => {
  expect(a + b).toBe(expected);
});

你可以使用 printf formatting 来格式化测试名称中的参数。

  • %s: String
  • %d: Number
  • %i: Integer
  • %f: Floating point value
  • %j: JSON
  • %o: Object
  • %#: 0-based index of the test case
  • %$: 1-based index of the test case
  • %%: Single percent sign ('%')
test.each([
  [1, 2, 3],
  [2, 2, 4],
])('adds %i + %i to equal %i', (a, b, sum) => {
  expect(a + b).toBe(sum);
});

// 此时将返回:
// adds 1 + 2 to equal 3
// adds 2 + 2 to equal 4

你也可以使用 $ 前缀访问对象属性和数组元素:

test.each([
  { a: 1, b: 1, sum: 2 },
  { a: 1, b: 2, sum: 3 },
  { a: 2, b: 1, sum: 3 },
])('adds $a + $b to equal $sum', ({ a, b, sum }) => {
  expect(a + b).toBe(sum);
});

// 此时将返回:
// adds 1 + 1 to equal 2
// adds 1 + 2 to equal 3
// adds 2 + 1 to equal 3

test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
])('add $0 + $1 to equal $2', (a, b, sum) => {
  expect(a + b).toBe(sum);
});

// 此时将返回:
// add 1 + 1 to equal 2
// add 1 + 2 to equal 3
// add 2 + 1 to equal 3

test.for

  • 类型: test.for(cases: ReadonlyArray<T>)(name: string, fn: (param: T, testContext: TestContext) => void | Promise<void>, timeout?: number) => void

test.each 的替代方案,提供 TestContext

test.for([
  { a: 1, b: 2 },
  { a: 2, b: 2 },
])('adds $a + $b', ({ a, b }, { expect }) => {
  expect(a + b).matchSnapshot();
});

test.for 同样支持标签模板字面量的表格语法:

test.for`
  a    | b    | expected
  ${1} | ${2} | ${3}
  ${2} | ${3} | ${5}
`('$a + $b = $expected', ({ a, b, expected }, { expect }) => {
  expect(a + b).toBe(expected);
});

你可以通过显式泛型参数来获得类型支持:

test.for<{ a: number; b: number; expected: number }>`
  a    | b    | expected
  ${1} | ${2} | ${3}
  ${2} | ${3} | ${5}
`('$a + $b = $expected', ({ a, b, expected }, { expect }) => {
  expect(a + b).toBe(expected);
});

test.fails

标记该测试预期会失败。

test.fails('should fail', () => {
  throw new Error('This test is expected to fail');
});

test.concurrent

并发运行连续带有 concurrent 标记的测试。

describe('suite', () => {
  test('serial test', async () => {
    /* ... */
  });
  test.concurrent('concurrent test 1', async () => {
    /* ... */
  });
  test.concurrent('concurrent test 2', async () => {
    /* ... */
  });
  test('serial test 1', async () => {
    /* ... */
  });
});

test.sequential

顺序(串行)运行测试(默认行为)。

describe('suite', () => {
  test('serial test', async () => {
    /* ... */
  });
  test('serial test 1', async () => {
    /* ... */
  });
});

test.runIf

仅当条件为真时才运行该测试。

test.runIf(process.env.RUN_EXTRA === '1')('conditionally run', () => {
  // ...
});

test.skipIf

当条件为真时跳过该测试。

test.skipIf(process.platform === 'win32')('skip on Windows', () => {
  // ...
});

test.extend

  • 类型: test.extend(fixtures: Fixtures)

通过自定义 fixture 扩展测试上下文,返回一个新的 test API。原始 test 不会被修改,你可以同时拥有多个各自独立的扩展版本。

Fixture 是可复用的上下文条目,用来准备测试资源并按需注入到测试里。常见用途包括:

  • 共享测试数据和辅助客户端(例如 API client、token、测试用户)。
  • 把 setup/teardown 逻辑集中在一处,避免每个测试重复写。
  • 构建 fixture 依赖(一个 fixture 可以依赖另一个 fixture)。
  • 通过 auto fixture 自动执行每个测试都需要的副作用(例如日志记录)。
import { test } from '@rstest/core';

const myTest = test.extend({
  user: async ({}, use) => {
    await use({ name: 'Alice' });
  },
});

// 使用 myTest(而非 test)来定义需要 fixture 的测试
myTest('has user in context', ({ user, expect }) => {
  expect(user.name).toBe('Alice');
});

// 原始 test 不受影响,也无法访问 user
test('plain test', ({ expect }) => {
  expect(1).toBe(1);
});

返回的 API 拥有与 test 相同的链式修饰符(onlyskipeachconcurrent 等),并且可以继续调用 .extend() 进行二次扩展。

Fixture 函数的生命周期

Fixture 函数接收两个参数:

  1. context — 包含其他 fixture 以及 TestContexttaskexpectonTestFinishedonTestFailed)。使用对象解构来声明你需要的依赖。
  2. use — 调用 await use(value) 把 fixture 的值传递给测试。

await use(value) 之前的代码是 setup,之后的代码是 teardown(测试结束后执行)。

import { test } from '@rstest/core';

const testWithDb = test.extend({
  db: async ({}, use) => {
    // setup:创建连接
    const db = await connectTestDb();
    await use(db);
    // teardown:关闭连接
    await db.close();
  },
});

普通值 fixture

如果 fixture 不需要 setup/teardown 逻辑,可以直接提供一个普通值:

import { test } from '@rstest/core';

const myTest = test.extend({
  baseURL: 'https://api.example.com',
});

myTest('uses baseURL', ({ baseURL, expect }) => {
  expect(baseURL).toBe('https://api.example.com');
});

访问 TestContext

Fixture 函数的第一个参数中同时包含 TestContext,因此你可以在 fixture 内直接读取当前测试信息或使用 expect

import { test } from '@rstest/core';

const myTest = test.extend({
  traceId: async ({ task }, use) => {
    // task.name 来自 TestContext
    await use(`trace:${task.name}`);
  },
});

Fixture 之间的依赖

一个 fixture 可以在第一个参数中解构其他 fixture,Rstest 会自动按依赖顺序初始化它们,并按反序执行 teardown:

import { test } from '@rstest/core';

const testWithApi = test.extend({
  baseURL: 'https://api.example.com',
  token: async ({ baseURL }, use) => {
    const token = await createTestToken(baseURL);
    await use(token);
    await revokeTestToken(token);
  },
});

testWithApi('fetch profile', async ({ baseURL, token, expect }) => {
  // baseURL → token 的初始化顺序由依赖关系自动决定
  const res = await fetch(`${baseURL}/profile`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  expect(res.ok).toBe(true);
});

自动 fixture(auto

Fixture 默认是按需执行的:只有测试函数中解构了它(或被其他 fixture 依赖)才会运行。如果你希望某个 fixture 对每个测试都自动生效——即使测试没有解构它——可以使用元组语法并设置 { auto: true }

import { test } from '@rstest/core';

const events: string[] = [];

const myTest = test.extend({
  logger: [
    async ({ task }, use) => {
      events.push(`start:${task.name}`);
      await use(undefined);
      events.push(`end:${task.name}`);
    },
    { auto: true },
  ],
});

myTest('runs logger automatically', ({ expect }) => {
  // 虽然没有解构 logger,但因为 auto: true,fixture 仍会执行
  expect(events).toContain('start:runs logger automatically');
});

类型推断与显式泛型

Fixture 的类型通常可以自动推断。如果推断不够精确,可以为 test.extend 传入显式泛型:

import { test } from '@rstest/core';

interface MyFixtures {
  user: { name: string; role: 'admin' | 'guest' };
}

const myTest = test.extend<MyFixtures>({
  user: async ({}, use) => {
    await use({ name: 'Alice', role: 'admin' });
  },
});

myTest('typed fixture', ({ user, expect }) => {
  // user 的类型为 { name: string; role: 'admin' | 'guest' }
  expect(user.role).toBe('admin');
});

注意:fixture 类型只在 test.extend 返回的新 API 上生效。原始 test 的类型签名不会改变。

Types

TestContext

TestContext 提供一些和当前测试有关的 API、上下文信息,以及自定义的 fixture。

export interface TestContext {
  /**
   * Metadata of the current test
   */
  task: {
    /**
     * A unique identifier for the test.
     * The format is `{fileHash}_{suiteIndex}_{testIndex}_...`, for example `419cefd87e_0_0`.
     */
    id: string;
    /** Test name provided by user */
    name: string;
    /** Result of the current test, undefined if the test is not run yet */
    result?: TestResult;
  };
  /** The `expect` API bound to the current test */
  expect: Expect;
  /** The `onTestFinished` hook bound to the current test */
  onTestFinished: OnTestFinished;
  /** The `onTestFailed` hook bound to the current test */
  onTestFailed: OnTestFailed;
}

你也可以通过 test.extend 方法通过自定义 fixture 的方式来扩展测试上下文。