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。遇到这类不稳定条件建议使用 rstest.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 匹配器用于验证 rstest.fn / rstest.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 = rstest.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 = rstest.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 = rstest.fn();
const after = rstest.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;
  }
}