close
  • 简体中文
  • Expect

    expect 用于在测试中创建断言。Rstest 提供了丰富的 API 及匹配器,支持轮询、快照断言等。

    你可以从 @rstest/core 包中导入 expect API:

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

    你也可以从 测试上下文 中获取 expect API,这有助于在并行测试中准确追踪断言的来源。

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

    expect

    • 类型: <T>(actual: T, message?: string) => Assertion<T>

    为给定的值创建一个断言对象。

    • actual:要断言的目标值。
    • message:可选,自定义失败提示信息,会在断言失败时显示。
    import { expect } from '@rstest/core';
    
    expect(1 + 1).toBe(2);
    expect('hello').toBeDefined();
    expect([1, 2, 3]).toContain(2);

    expect.element(Browser Mode)

    • 类型: (locator: Locator) => BrowserElementExpect

    在 Browser Mode 中,expect.element 用于对 @rstest/browser 提供的 Locator 执行断言(例如 toBeVisibletoHaveTexttoHaveValue)。

    import { page } from '@rstest/browser';
    import { expect, test } from '@rstest/core';
    
    test('asserts by locator', async () => {
      document.body.innerHTML = '<button>Save</button>';
      await expect
        .element(page.getByRole('button', { name: 'Save' }))
        .toBeVisible();
    });

    expect.element 仅在 Browser Mode 中可用,并且需要引入 @rstest/browser 来安装浏览器侧适配层。完整参考见 Assertion(Browser Mode)

    expect.not

    否定该断言。

    expect(1 + 1).not.toBe(3);
    expect('foo').not.toBeUndefined();

    expect.soft

    • 类型: <T>(actual: T, message?: string) => Assertion<T>

    即使断言失败,测试也会继续执行,所有失败会在最后统一报告。

    expect.soft(1 + 1).toBe(3); // 不会中断测试
    expect.soft(1 + 2).toBe(4);

    expect.poll

    • 类型: <T>(actual: () => T | Promise<T>, options?: { interval?: number, timeout?: number, message?: string }) => PromiseLike<Assertion<Awaited<T>>>

    持续调用 actual,并重复执行 matcher,直到断言通过或超时。

    • 默认 interval 50(毫秒)
    • 默认 timeout 1000(毫秒)

    options

    • interval:每次重试之间的间隔时间(毫秒)。
    • timeout:轮询允许的最长总耗时(毫秒)。
    • message:轮询断言失败时显示的自定义提示信息。

    注意:

    • expect.poll(...) 是异步断言,必须使用 await
    • expect.poll(...) 不支持 resolves/rejectstoThrow* 以及快照类 matcher。遇到这类不稳定条件建议使用 rs.waitFor()
    await expect.poll(() => getStatus()).toBe('ready');
    
    await expect
      .poll(
        async () => {
          const response = await fetch('/api/health');
          const data = await response.json();
          return data.status;
        },
        {
          interval: 100,
          timeout: 5000,
          message: '服务应在 5 秒内恢复健康',
        },
      )
      .toBe('healthy');

    expect.unreachable

    • 类型: (message?: string) => never

    标记代码路径为不可达。如果调用会抛出异常。

    if (shouldNotHappen) {
      expect.unreachable('这里不应该被执行');
    }

    expect.assertions

    • 类型: (expected: number) => void

    验证在测试期间调用了特定数量的断言。常用来检查异步代码是否被调用或是否触发了预期的异常。

    expect.assertions(1);
    
    try {
      await someAsyncFunction();
    } catch (e) {
      expect(e).toBeInstanceOf(Error);
    }

    expect.hasAssertions

    • 类型: () => void

    验证在测试期间至少调用了一个断言。

    适用于断言次数不固定,但必须至少发生一次断言的场景。

    expect.hasAssertions();
    expect(1 + 1).toBe(2);

    expect.addEqualityTesters

    • 类型: (testers: Array<Tester>) => void

    自定义用来验证两个对象是否相等的测试器。

    每个 tester 可以返回:

    • true:两者视为相等
    • false:两者视为不相等
    • undefined:跳过当前 tester,交给下一个 tester 或默认比较逻辑
    expect.addEqualityTesters([
      (a, b) => {
        if (typeof a === 'number' && typeof b === 'number') {
          return Math.abs(a - b) < 0.01;
        }
      },
    ]);
    expect(0.1 + 0.2).toEqual(0.3); // 使用自定义测试器后为 true

    expect.addSnapshotSerializer

    • 类型: (serializer: SnapshotSerializer) => void

    为快照测试添加自定义序列化工具。

    expect.addSnapshotSerializer({
      test: (val) => typeof val === 'string' && val.startsWith('secret:'),
      print: (val) => '***MASKED***',
    });
    expect('secret:123').toMatchSnapshot(); // 快照输出的 secret 信息会被掩码

    expect.getState / expect.setState

    • 类型:
      • getState: () => MatcherState
      • setState: (state: Partial<MatcherState>) => void

    获取或设置内部匹配器状态。

    const state = expect.getState();
    console.log(state.currentTestName);
    expect.setState({ currentTestName: '自定义名称' });
    console.log(expect.getState().currentTestName); // 输出 '自定义名称'

    匹配器(Matchers)

    常用匹配器

    常用匹配器是最常见的一组断言能力,用来覆盖值比较、结构比较、类型判断和异常验证。

    相等性校验:

    • toBe(value):基于 Object.is 的严格相等比较。
    • toEqual(value):对象/数组的深度相等比较。
    • toStrictEqual(value):更严格的深度相等(包含 undefined 字段、稀疏数组差异)。
    • toBeOneOf(array):值必须命中给定候选集合之一。

    类型与存在性:

    • toBeTruthy() / toBeFalsy():真值/假值判断。
    • toBeNull() / toBeUndefined() / toBeDefined():空值与定义状态判断。
    • toBeNaN():判断是否为 NaN
    • toBeInstanceOf(class):实例类型判断。
    • toBeTypeOf(type):运行时类型判断(如 'string')。

    数字比较:

    • toBeGreaterThan(number) / toBeGreaterThanOrEqual(number)
    • toBeLessThan(number) / toBeLessThanOrEqual(number)
    • toBeCloseTo(number, numDigits?):适合浮点数近似比较。

    集合与字符串:

    • toContain(item):数组/字符串包含判断。
    • toContainEqual(item):数组成员的深度相等判断。
    • toMatch(stringOrRegExp):字符串匹配子串或正则。
    • toMatchObject(object):对象包含指定属性子集。
    • toHaveLength(length):断言 .length
    • toHaveProperty(path, value?):断言属性路径存在,可选断言该路径值。

    自定义条件与异常:

    • toSatisfy(fn):满足自定义谓词函数。
    • toThrowError(expected?):断言函数抛错(支持字符串/正则/错误类型)。
    // toBe vs toEqual
    expect(1 + 1).toBe(2);
    expect({ id: 1, name: 'a' }).toEqual({ id: 1, name: 'a' });
    
    // 严格深比较
    expect({ a: undefined, b: 1 }).toStrictEqual({ a: undefined, b: 1 });
    
    // 数字断言
    expect(0.1 + 0.2).toBeCloseTo(0.3, 5);
    expect(10).toBeGreaterThan(5);
    
    // 集合断言
    expect([1, 2, 3]).toContain(2);
    expect([{ id: 1 }]).toContainEqual({ id: 1 });
    expect({ user: { role: 'admin' } }).toHaveProperty('user.role', 'admin');
    expect({ id: 1, role: 'admin', active: true }).toMatchObject({
      id: 1,
      role: 'admin',
    });
    
    // 文本与自定义条件
    expect('request id: 42').toMatch(/id:\s\d+/);
    expect(21).toSatisfy((n) => n % 3 === 0);
    
    // 异常断言(必须包成函数)
    expect(() => {
      throw new TypeError('invalid input');
    }).toThrowError(TypeError);

    Mock 匹配器

    Mock 匹配器用于验证 rs.fn / rs.spyOn 的交互行为,包括调用次数、参数、调用顺序、返回值和异步结果。

    调用相关:

    • toHaveBeenCalled():至少被调用一次。
    • toHaveBeenCalledTimes(times):被调用次数精确匹配。
    • toHaveBeenCalledWith(...args):任意一次调用参数匹配。
    • toHaveBeenLastCalledWith(...args):最后一次调用参数匹配。
    • toHaveBeenNthCalledWith(n, ...args):第 n 次调用参数匹配。
    • toHaveBeenCalledExactlyOnceWith(...args):仅调用一次且参数匹配。
    • toHaveBeenCalledBefore(mock) / toHaveBeenCalledAfter(mock):校验调用先后顺序。

    返回值相关:

    • toHaveReturned():至少有一次正常返回。
    • toHaveReturnedTimes(times):正常返回次数精确匹配。
    • toHaveReturnedWith(value):任意一次返回值匹配。
    • toHaveLastReturnedWith(value):最后一次返回值匹配。
    • toHaveNthReturnedWith(n, value):第 n 次返回值匹配。

    Promise 结果相关:

    • toHaveResolved():至少有一次 resolve。
    • toHaveResolvedTimes(times):resolve 次数精确匹配。
    • toHaveResolvedWith(value):任意一次 resolve 值匹配。
    • toHaveLastResolvedWith(value):最后一次 resolve 值匹配。
    • toHaveNthResolvedWith(n, value):第 n 次 resolve 值匹配。

    注意:

    • Nth 系列从 1 开始计数(1 表示第一次调用)。
    • toHaveBeenCalledWith 只要求“某次”匹配;需要严格位置时用 LastNth
    • 这些 matcher 只能用于 mock/spy,不能用于普通函数。
    const sum = rs.fn((a: number, b: number) => a + b);
    sum(1, 2);
    sum(2, 3);
    
    expect(sum).toHaveBeenCalledTimes(2);
    expect(sum).toHaveBeenNthCalledWith(1, 1, 2);
    expect(sum).toHaveBeenLastCalledWith(2, 3);
    expect(sum).toHaveReturnedWith(3);
    expect(sum).toHaveLastReturnedWith(5);
    
    const fetchUser = rs.fn(async (id: number) => ({ id, name: 'alice' }));
    await fetchUser(1);
    await fetchUser(2);
    
    expect(fetchUser).toHaveResolvedTimes(2);
    expect(fetchUser).toHaveNthResolvedWith(1, { id: 1, name: 'alice' });
    
    const before = rs.fn();
    const after = rs.fn();
    before();
    after();
    expect(before).toHaveBeenCalledBefore(after);

    快照匹配器

    快照匹配器用于将值、错误或文件与之前记录的快照进行比较,便于追踪输出的变化。

    • toMatchSnapshot():将值与已保存的快照进行比较。
    • toMatchInlineSnapshot():将值与测试文件中的内联快照进行比较。
    • toThrowErrorMatchingSnapshot():检查抛出的错误与已保存快照匹配。
    • toThrowErrorMatchingInlineSnapshot():检查抛出的错误与内联快照匹配。
    • toMatchFileSnapshot(filepath):将值与指定文件中的快照进行比较。
    expect('hello world').toMatchSnapshot();
    
    expect(() => {
      throw new Error('fail');
    }).toThrowErrorMatchingSnapshot();
    
    await expect('hello world').toMatchFileSnapshot(
      '__snapshots__/file.output.txt',
    );

    Promise 匹配器

    • resolves:对 Promise 的 resolve 结果进行断言。
    • rejects:对 Promise 的 reject 结果进行断言。
    await expect(Promise.resolve('ok')).resolves.toBe('ok');
    await expect(Promise.reject(new Error('fail'))).rejects.toThrow('fail');

    不对称匹配器

    不对称匹配器是一些辅助工具,允许更灵活地匹配值,如部分匹配、类型匹配或模式匹配,适合编写更具表现力且不易碎的测试。

    • expect.anything():匹配除 null 和 undefined 之外的任意值。
    • expect.any(constructor):匹配指定类型的任意值。
    • expect.closeTo(number, precision?):匹配接近期望值的数字。
    • expect.arrayContaining(array):匹配包含期望元素的数组。
    • expect.objectContaining(object):匹配包含期望属性的对象。
    • expect.stringContaining(string):匹配包含期望子串的字符串。
    • expect.stringMatching(stringOrRegExp):匹配符合期望模式的字符串。
    expect({ a: 1 }).toEqual({ a: expect.anything() });
    
    expect(1).toEqual(expect.any(Number));
    
    expect(0.1 + 0.2).toEqual(expect.closeTo(0.3, 5));
    
    expect([1, 2, 3]).toEqual(expect.arrayContaining([2, 1]));
    
    expect({ a: 1, b: 2 }).toEqual(expect.objectContaining({ a: 1 }));
    
    expect('hello world').toEqual(expect.stringContaining('world'));
    
    expect('hello world').toEqual(expect.stringMatching(/^hello/));

    自定义匹配器

    你可以通过扩展 expect 添加自定义匹配器:

    expect.extend({
      toBeDivisibleBy(received, argument) {
        const pass = received % argument === 0;
        if (pass) {
          return {
            message: () => `期望 ${received} 不能被 ${argument} 整除`,
            pass: true,
          };
        } else {
          return {
            message: () => `期望 ${received} 能被 ${argument} 整除`,
            pass: false,
          };
        }
      },
    });
    
    expect(10).toBeDivisibleBy(2);

    如果你想为自定义匹配器添加 TypeScript 类型定义,可以扩展 @rstest/core 模块中的 Assertion 接口声明:

    declare module '@rstest/core' {
      interface Assertion<T> {
        toBeDivisibleBy(argument: number): void;
      }
    }