$npx -y skills add weAAAre/a11y-agents-kit --skill rstestExpert guidance for writing, configuring, and running tests with Rstest — the Rspack-powered. JavaScript/TypeScript testing framework with Jest-compatible APIs and native ESM/TypeScript support. Covers setup, configuration (rstest.config.ts), mocking (rs.mock, rstest.fn, rstest.s
| 1 | # Rstest |
| 2 | |
| 3 | Rstest is a testing framework powered by Rspack. It provides Jest-compatible APIs with native TypeScript and ESM support, and integrates directly into the Rstack toolchain (Rspack, Rsbuild, Rslib, Rspress). |
| 4 | |
| 5 | Key things that distinguish Rstest from Jest/Vitest: |
| 6 | |
| 7 | - **Build tooling is Rspack**, not Babel or Vite — it reuses your existing Rsbuild/Rspack config |
| 8 | - **Two separate utility namespaces**: `rs` for module-level operations (mocking modules), and `rstest` for runtime utilities (fn, spyOn, timers). Both are imported from `@rstest/core` |
| 9 | - **`rs.mock()` without a factory is NOT auto-mock** — it looks for `__mocks__/` directory only. For auto-mocking, pass `{ mock: true }`. This is the biggest gotcha when migrating from Vitest |
| 10 | - **Mock factories cannot be async** — use `import ... with { rstest: 'importActual' }` for partial mocks instead |
| 11 | - **No `--run` flag** — `rstest` already runs once and exits. Use `rstest --watch` or `rstest watch` for watch mode |
| 12 | - Node.js ≥ 20.19.0 required |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## Setup |
| 17 | |
| 18 | ### Install |
| 19 | |
| 20 | ```bash |
| 21 | # npm / pnpm / yarn / bun |
| 22 | pnpm add @rstest/core -D |
| 23 | ``` |
| 24 | |
| 25 | ### package.json scripts |
| 26 | |
| 27 | ```json |
| 28 | { |
| 29 | "scripts": { |
| 30 | "test": "rstest", |
| 31 | "test:watch": "rstest watch" |
| 32 | } |
| 33 | } |
| 34 | ``` |
| 35 | |
| 36 | ### Configuration file |
| 37 | |
| 38 | Rstest auto-discovers config files in the project root in this order: `rstest.config.mjs`, `.ts`, `.js`, `.cjs`, `.mts`, `.cts`. |
| 39 | |
| 40 | ```ts |
| 41 | // rstest.config.ts |
| 42 | import { defineConfig } from '@rstest/core'; |
| 43 | |
| 44 | export default defineConfig({ |
| 45 | // Test config is at the top level — NOT nested under a `test` key (unlike Vitest) |
| 46 | include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], |
| 47 | exclude: ['**/node_modules/**', '**/dist/**'], |
| 48 | testEnvironment: 'node', // 'node' | 'jsdom' | 'happy-dom' |
| 49 | globals: false, // set true to skip imports in test files |
| 50 | setupFiles: [], // runs before each test file |
| 51 | testTimeout: 5000, |
| 52 | retry: 0, |
| 53 | }); |
| 54 | ``` |
| 55 | |
| 56 | If your project already uses Rsbuild or Rslib, use the official adapters to avoid config duplication — see "Rsbuild / Rslib integration" below. |
| 57 | |
| 58 | --- |
| 59 | |
| 60 | ## Writing tests |
| 61 | |
| 62 | ### Imports |
| 63 | |
| 64 | ```ts |
| 65 | import { describe, test, it, expect, beforeAll, afterAll, beforeEach, afterEach } from '@rstest/core'; |
| 66 | import { rs } from '@rstest/core'; // module-level mocking |
| 67 | import { rstest } from '@rstest/core'; // runtime utilities (fn, spyOn, timers) |
| 68 | ``` |
| 69 | |
| 70 | When `globals: true` is set, all of these are available globally without imports. |
| 71 | |
| 72 | ### Basic test |
| 73 | |
| 74 | ```ts |
| 75 | import { expect, test } from '@rstest/core'; |
| 76 | |
| 77 | test('adds numbers', () => { |
| 78 | expect(1 + 2).toBe(3); |
| 79 | }); |
| 80 | ``` |
| 81 | |
| 82 | ### Grouping |
| 83 | |
| 84 | ```ts |
| 85 | import { describe, expect, test } from '@rstest/core'; |
| 86 | |
| 87 | describe('math utils', () => { |
| 88 | test('adds', () => expect(add(1, 2)).toBe(3)); |
| 89 | test('subtracts', () => expect(sub(3, 1)).toBe(2)); |
| 90 | }); |
| 91 | ``` |
| 92 | |
| 93 | ### Async tests |
| 94 | |
| 95 | ```ts |
| 96 | test('fetches user', async () => { |
| 97 | const user = await fetchUser(1); |
| 98 | expect(user.name).toBe('Alice'); |
| 99 | }); |
| 100 | ``` |
| 101 | |
| 102 | ### Parameterized tests |
| 103 | |
| 104 | ```ts |
| 105 | // .each (printf-style) |
| 106 | test.each([ |
| 107 | [1, 2, 3], |
| 108 | [0, 0, 0], |
| 109 | ])('add(%i, %i) = %i', (a, b, expected) => { |
| 110 | expect(add(a, b)).toBe(expected); |
| 111 | }); |
| 112 | |
| 113 | // .for (type-safe, modern) |
| 114 | test.for([ |
| 115 | { a: 1, b: 2, expected: 3 }, |
| 116 | ])('add($a, $b) = $expected', ({ a, b, expected }) => { |
| 117 | expect(add(a, b)).toBe(expected); |
| 118 | }); |
| 119 | ``` |
| 120 | |
| 121 | ### Modifiers |
| 122 | |
| 123 | ```ts |
| 124 | test.skip('disabled', () => { /* ... */ }); |
| 125 | test.only('focus', () => { /* ... */ }); |
| 126 | test.todo('implement later'); |
| 127 | test.fails('expected to throw', () => { throw new Error(); }); |
| 128 | test.concurrent('runs in parallel', async () => { /* ... */ }); |
| 129 | test.sequential('forced serial', async () => { /* ... */ }); |
| 130 | ``` |
| 131 | |
| 132 | ### Lifecycle hooks |
| 133 | |
| 134 | ```ts |
| 135 | beforeAll(() => { /* once before suite */ }); |
| 136 | afterAll(() => { /* once after suite */ }); |
| 137 | beforeEach(() => { /* before each test */ }); |
| 138 | afterEach(() => { /* after each test */ }); |
| 139 | ``` |
| 140 | |
| 141 | The return value of `beforeEach`/`beforeAll` is used as a cleanup function (runs after) — make sure to wrap void calls in braces: `beforeEach(() => { do |