close
  • 简体中文
  • Test

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

    别名:it

    test

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

    定义一个测试用例。

    import { expect, test } from '@rstest/core';
    
    test('should add two numbers correctly', () => {
      expect(1 + 1).toBe(2);
      expect(1 + 2).toBe(3);
    });

    TestOptions

    TestOptions 对象作为第二个参数(位于测试函数之前)传入,用于细化单个测试的运行行为:

    test('flaky network call', { retry: 3 }, async () => {
      /* ... */
    });

    作为简写,你仍可以将数字作为最后一个参数传入,仅用于设置超时(等价于 { timeout: n }):

    test('runs within 3s', async () => {
      /* ... */
    }, 3000);

    TestOptions 支持以下字段:

    • timeout?: number — 单测超时(毫秒),覆盖 test.testTimeout
    • retry?: number — 测试失败时的重跑次数,首次通过即停。覆盖 test.retry
    • repeats?: number — 在通过的前提下额外重跑指定次数,任一次失败则整体判定为失败。每次重跑会完整执行 beforeEach / afterEach,并各自享有独立的 retry 配额。
    test('flaky network call', { retry: 3 }, async () => {
      /* ... */
    });
    
    // 共执行 10 次;任一次失败即短路后续重跑
    test('stays green across runs', { repeats: 9 }, async () => {
      /* ... */
    });

    test.eachtest.for 的第二个参数语义相同,会作用于生成的每个 case。

    test.only

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

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

    test.skip

    跳过某些测试。

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

    当你在定义测试时就确定要跳过它,可以使用 test.skip。如果只能在测试运行过程中决定是否跳过,请从测试上下文中调用 context.skip()context.skip() 会立即停止执行当前测试,因此它后面的代码不会继续执行,并且该测试会被报告为 skipped。

    test('skip at runtime', (context) => {
      context.skip();
    
      // 这个断言不会被执行。
      expect(1 + 1).toBe(3);
    });

    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(cases: ReadonlyArray<T>)(name: string, options: TestOptions, fn?: (param: T) => void | Promise<void>): 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.for

    • 类型:
    test.for(cases: ReadonlyArray<T>)(name: string, fn?: (param: T, testContext: TestContext) => void | Promise<void>, timeout?: number): void;
    test.for(cases: ReadonlyArray<T>)(name: string, options: TestOptions, fn?: (param: T, testContext: TestContext) => void | Promise<void>): 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;
      /** Skip the current test during execution */
      skip: () => never;
      /** The `onTestFinished` hook bound to the current test */
      onTestFinished: OnTestFinished;
      /** The `onTestFailed` hook bound to the current test */
      onTestFailed: OnTestFailed;
    }

    你可以使用 context.skip() 在测试运行过程中跳过当前测试。context.skip() 之后的代码不会继续执行,测试结果会被标记为 skipped:

    import { expect, test } from '@rstest/core';
    
    test('skipped test', (context) => {
      context.skip();
    
      expect(1 + 1).toBe(3);
    });

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