> For AI agents: the complete documentation index is available at https://rspack.rs/llms.txt, the full documentation bundle is available at https://rspack.rs/llms-full.txt.

# Testing

Because Rspack uses a mix of Rust and Node.js code, different testing strategies are used for each.

## Rust testing

:::tip
Rust test cases are only suitable for unit testing. To test the complete build process, please add Node.js test cases.
:::

### Running Rust tests

You can run the Rust code's test cases using `./x test rust` or `cargo test`.

### Writing Rust tests

Test cases are written within the Rust code. For example:

```rust
fn add(a: u8, b: u8) -> u8 {
  a + b
}

#[test]
fn test_add() {
  assert_eq!(add(1, 2), 3);
}
```

> For more information, please refer to: [Rust: How to Write Tests](https://doc.rust-lang.org/book/ch11-01-writing-tests.html)

## Node.js testing

Rspack's test cases include the following:

- Rspack core test cases are stored in the `tests/rspack-test` folder and will run the test cases by simulating the build process. In general, test cases should be added in this folder.
- Test cases for other Rspack packages are stored in the `packages/{name}/tests` folder and should only be added or modified when modifying that package.

You can run Rspack tests by running `./x test unit` or `pnpm run test:unit` at the root folder.

You can also go to the `tests/rspack-test` folder and run `npm run test` to run test cases and add some arguments:

- **When refreshing test snapshots is needed**: Add `-u`, like `npm run test -- -u`
- **When filtering test cases is needed**: Add `-t`. Pattern matching supports regex, see [rstest](https://rstest.rs/guide/basic/test-filter#filter-by-test-name) for details.

### Running tests

You can run these test cases in the following ways:

- Run `./x test unit` or `pnpm run test:unit` from the root directory.
- Or run `npm run test` from the `tests/rspack-test` directory.
- To update snapshots, run `npm run test -- -u` from the `tests/rspack-test` directory.
- To pass specific rstest cli arguments, run `npm run test -- {args}` from the `tests/rspack-test` directory.
- To filter specific test cases, run `npm run test -- -t ${testPath}` where `testPath` can be either an absolute or relative path.
  - For example:
    - `npm run test -- -t hotCases/json/error-in-json` (or `npm run test -- -t /Users/rspack/tests/rspack-test/hotCases/json/error-in-json`) will run all tests in the `error-in-json` test case.
    - `npm run test -- -t hotCases/json` (or `npm run test -- -t /Users/rspack/tests/rspack-test/hotCases/json`) will run all test cases in the `json` directory.
    - `npm run test -- -t hotCases` (or `npm run test -- -t /Users/rspack/tests/rspack-test/hotCases`) will run all test cases in the `hotCases` directory.
- To use Rspack Wasm for running test cases, you need to additionally configure the following environment variables:
  1. `NAPI_RS_FORCE_WASI=1`: Forces the use of Rspack Wasm instead of native binding
  2. `WASM=1`: Enables Wasm-specific test configurations
  3. `NODE_NO_WARNINGS=1`: Disables Node.js Wasm warnings.
  ```bash
  NAPI_RS_FORCE_WASI=1 WASM=1 NODE_NO_WARNINGS=1 pnpm run test:unit
  ```

### Directory structure

The structure of the `tests/rspack-test` folder is as follows:

```bash
.
├── js # Used to store build artifacts and temporary files
├── __snapshots__ # Used to store test snapshots
├── {Name}.test.js # Entry for normal testing
├── {Name}.hottest.js # Entry for hot snapshot testing
├── {Name}.difftest.js # Entry for diff testing
├── {name}Cases # Directory to store test cases
└── fixtures # General test files
```

The `{Name}.test.js` is the entry file for tests, which will walk the `{name}Cases` folder and run cases in it. Therefore, when you need to add or modify test cases, add them to the relevant `{name}Cases` folder based on the type of testing.

### Test types

The existing test types are:

- [Normal](#normal): Used to test core build processes without configuration changes. This type is used when testing does not require adding `rspack.config.js`.
- [Config](#config): Used to test build configuration options. If your test needs specific configuration added through `rspack.config.js` to run and does not fit other scenarios, use this test type.
- [Hot](#hot): Used to test whether HMR runs correctly. This type includes HotNode with a fixed `target=async-node`, HotWeb with a fixed `target=web`, and HotWorker with a fixed `target=webworker`.
- [HotSnapshot](#hotsnapshot): Used to test whether HMR can generate correct intermediate artifacts. This test type shares test cases with the Hot type and generates snapshots for incremental artifacts for each HMR.
- [Watch](#watch): Used to test incremental compilation after modifying files in Watch mode.
- [StatsOutput](#statsoutput): Used to test the console output log after the build ends.
- [StatsAPI](#stats-api): Used to test the Stats object generated after the build ends.
- [Diagnostic](#diagnostic): Used to test the formatted output information for warnings/errors generated during the build process.
- [Hash](#hash): Used to test whether hash generation works correctly.
- [Compiler](#compiler): Used to test Compiler/Compilation object APIs.
- [Defaults](#defaults): Used to test the interaction between configuration options.
- [Error](#error): Used to test the interaction between `compilation.errors` and `compilation.warnings`.
- [Hook](#hook): Used to test various hook functionalities.
- [TreeShaking](#treeshaking): Used to test Tree Shaking-related features.
- [Builtin](#builtin): Used to test plugins with built-in native implementations.

Please prioritize adding test cases within the above test types.

### Normal

| Test Entry            | `tests/Normal.test.js`                                                                                             |
| --------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Case Directory        | [`tests/normalCases`](https://github.com/web-infra-dev/rspack/tree/main/tests/rspack-test/normalCases)             |
| Output Directory      | `tests/js/normal`                                                                                                  |
| Default Configuration | [NormalProcessor](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/normal.ts) |
| Run Output            | `Yes`                                                                                                              |

The writing of the case is the same as a regular rspack project, but it does not include the `rspack.config.js` file and will use the provided configuration for building.

### Config

| Test Entry            | `tests/Config.test.js`                                                                                             |
| --------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Case Directory        | [`tests/configCases`](https://github.com/web-infra-dev/rspack/tree/main/tests/rspack-test/configCases)             |
| Output Directory      | `tests/js/config`                                                                                                  |
| Default Configuration | [ConfigProcessor](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/config.ts) |
| Run Output            | `Yes`                                                                                                              |

This test case is similar to a regular rspack project. You can specify the build configuration by adding a `rspack.config.js` and control various behaviors during testing by adding a `test.config.js`. The structure of the `test.config.js` file is as follows:

```ts {16-20} title="test.config.js"
type TConfigCaseConfig = {
  noTests?: boolean; // Do not run the test output and end the test
  beforeExecute?: () => void; // Callback before running the output
  afterExecute?: () => void; // Callback after running the output
  moduleScope?: (ms: IModuleScope) => IModuleScope; // Module context variables when running the output
  findBundle?: (
    // Function for obtaining output when running the output, can control the output at a finer granularity
    index: number, // Compiler index in multi-compiler scenario
    options: TCompilerOptions<T>, // Build configuration object
  ) => string | string[];
  bundlePath?: string[]; // Output file name when running the output (prior to findBundle)
  nonEsmThis?: (p: string | string[]) => Object; // this object during CJS output runtime, defaults to current module's module.exports if not specified
  modules?: Record<string, Object>; // Pre-added modules when running the output, will be prioritized when required
  timeout?: number; // Timeout for the test case
};

/** @type {import("../../../..").TConfigCaseConfig} */
module.exports = {
  // ...
};
```

### Hot

| Test Entry            | `Hot{Target}.test.js`                                                                                        |
| --------------------- | ------------------------------------------------------------------------------------------------------------ |
| Case Directory        | [`tests/hotCases`](https://github.com/web-infra-dev/rspack/tree/main/tests/rspack-test/hotCases)             |
| Output Directory      | `tests/js/hot-{target}`                                                                                      |
| Default Configuration | [HotProcessor](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/hot.ts) |
| Run Output            | `Yes`                                                                                                        |

This test case is similar to a regular rspack project. You can specify the build configuration by adding a `rspack.config.js`.

And also, within the file that has changed, use `---` to separate the code before and after the change:

```js file.js title="file.js"
module.exports = 1; // Initial build
---
module.exports = 2; // First hot update
---
module.exports = 3; // Second hot update
```

In the test case code, use the `NEXT_HMR` method to control the timing of file changes and add test code within it:

```js title="index.js"
import value from './file';

it('should hot update', done => {
  expect(value).toBe(1);
  // Use tests/rspack-test/hotCases/update.js to trigger update
  await NEXT_HMR();
  expect(value).toBe(2);
  await NEXT_HMR();
  expect(value).toBe(3);
});

module.hot.accept('./file');
```

### HotSnapshot

| Test Entry            | `HotSnapshot.hottest.js`                                                                         |
| --------------------- | ------------------------------------------------------------------------------------------------ |
| Case Directory        | [`tests/hotCases`](https://github.com/web-infra-dev/rspack/tree/main/tests/rspack-test/hotCases) |
| Output Directory      | `tests/js/hot-snapshot`                                                                          |
| Default Configuration | Same as [Hot](#hot)                                                                              |
| Run Output            | `Yes`                                                                                            |

Uses the same test cases as `Hot{Target}`, and generates a `__snapshots__/{target}/{step}.snap.txt` file in the case folder to perform snapshot testing on the incremental artifacts of each HMR.

The snapshot structure is as follows:

- **Changed Files**: Source code files that trigger this HMR build
- **Asset Files**: Artifact files of this HMR build
- **Manifest**: Contents of the `hot-update.json` metadata file for this HMR build. See [Hot update manifest](https://rspack.rs/plugins/hot-module-replacement-plugin.md#hot-update-manifest) for the meaning of each field.
- **Update**: Information about the `hot-update.js` patch file for this HMR build, including:
  - **Changed Modules**: List of modules included in the patch
  - **Changed Runtime Modules**: List of runtime modules included in the patch
  - **Changed Content**: Snapshot of the patch code

### Watch

| Entry File            | `Watch.test.js`                                                                                                  |
| --------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Case Directory        | [`tests/watchCases`](https://github.com/web-infra-dev/rspack/tree/main/tests/rspack-test/watchCases)             |
| Output Directory      | `tests/js/watch`                                                                                                 |
| Default Configuration | [WatchProcessor](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/watch.ts) |
| Run Output            | `Yes`                                                                                                            |

As the Watch build needs to be performed in multiple steps, you can specify the build configuration by adding a `rspack.config.js`. The directory structure of its cases is special and will use incrementing numbers to represent change batches:

```bash
.
├── 0 # WATCH_STEP=0, initial code for the case
├── 1 # WATCH_STEP=1, diff files for the first change
├── 2 # WATCH_STEP=2, diff files for the second change
└── rspack.config.js
```

In the test code, you can use the `WATCH_STEP` variable to get the current batch number of changes.

### StatsOutput

| Test Entry            | `StatsOutput.test.js`                                                                                                   |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Case Directory        | [`tests/statsOutputCases`](https://github.com/web-infra-dev/rspack/tree/main/tests/rspack-test/statsOutputCases)        |
| Output Directory      | `tests/js/statsOutput`                                                                                                  |
| Default Configuration | [StatsProcessor](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/stats-output.ts) |
| Run Output            | `No`                                                                                                                    |

The writing of the cases is the same as in a regular rspack project. After running, the console output information will be captured in snapshots and stored in `tests/rspack-test/__snapshots__/StatsOutput.test.js.snap`.

:::tip
As some StatsOutput test cases contain hashes, when you modify the output code, please use the `-u` parameter to update the snapshots for these cases.
:::

### Stats API

| Entry File            | `StatsAPI.test.js`                                                                                           |
| --------------------- | ------------------------------------------------------------------------------------------------------------ |
| Case Directory        | `tests/statsAPICases`                                                                                        |
| Output Directory      | `None`                                                                                                       |
| Default Configuration | [`None`](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/stats-api.ts) |
| Run Output            | `No`                                                                                                         |

This test uses `tests/rspack-test/fixtures` as the source code for the build, so the test case is written as a single file. Its structure is as follows:

```js title="{case}.js"
type TStatsAPICaseConfig = {
  description: string, // Case description
  options?: (context: ITestContext) => TCompilerOptions<T>, // Case build configuration
  build?: (context: ITestContext, compiler: TCompiler<T>) => Promise<void>, // Case build method
  check?: (stats: TCompilerStats<T>, compiler: TCompiler<T>) => Promise<void>, // Function to check the stats for the case
};

// [!code highlight:4]
/** @type {import('../..').TStatsAPICaseConfig} */
module.exports = {
  // ...
};
```

### Diagnostic

| Entry File            | `Diagnostics.test.js`                                                                                                      |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Case Directory        | [`tests/diagnosticsCases`](https://github.com/web-infra-dev/rspack/tree/main/tests/rspack-test/diagnosticsCases)           |
| Output Directory      | `tests/js/diagnostics`                                                                                                     |
| Default Configuration | [DiagnosticProcessor](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/diagnostic.ts) |
| Run Output            | `No`                                                                                                                       |

This test case is similar to a typical rspack project and can specify build configurations by adding a `rspack.config.js`. Additionally, it will add a `stats.err` file in the case directory to store snapshots of warnings/errors. To refresh, use the `-u` parameter.

### Hash

| Entry File            | `Hash.test.js`                                                                                                 |
| --------------------- | -------------------------------------------------------------------------------------------------------------- |
| Case Directory        | [`tests/hashCases`](https://github.com/web-infra-dev/rspack/tree/main/tests/rspack-test/hashCases)             |
| Output Directory      | None                                                                                                           |
| Default Configuration | [HashProcessor](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/hash.ts) |
| Run Output            | No                                                                                                             |

This test case is similar to a typical rspack project, but it will add a `test.config.js` file in the case directory and specify a `validate()` method to check the hash information in the `stats` object after the build is complete:

```js title="test.config.js"
type THashCaseConfig = {
  validate?: (stats: TCompilerStats<T>) => void,
};

// [!code highlight:4]
/** @type {import('../..').THashCaseConfig} */
module.exports = {
  // ...
};
```

### Compiler

| Entry File            | `Compiler.test.mjs`                                                                                        |
| --------------------- | ---------------------------------------------------------------------------------------------------------- |
| Case Directory        | [`tests/compilerCases`](https://github.com/web-infra-dev/rspack/tree/main/tests/rspack-test/compilerCases) |
| Output Directory      | None                                                                                                       |
| Default Configuration | [None](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/compiler.ts)  |
| Run Output            | No                                                                                                         |

Compiler cases support both existing single files and self-contained directories. Single-file cases use `tests/rspack-test/fixtures` as their default build context. A directory case uses its own directory and must contain `test.config.mjs`, `test.config.cjs`, or `test.config.js` (in that priority order). Only the config is discovered; sibling inputs, loaders, and helpers are not tests.

The config exports one `TCompilerCaseConfig` or an array. Ordinary cases use `options`, `compiler`, `build`, and `check` to customize the managed compiler. For GC tests or scenarios that manage their own compilers, use `run` instead:

```js title="object-lifecycle/test.config.mjs"
/** @type {import('@rspack/test-tools').TCompilerCaseConfig[]} */
export default [
  {
    name: 'hooks',
    description: 'should collect hook closures and captured objects',
    async run() {
      const { default: run } = await import('./hooks.mjs');
      await run();
    },
  },
];
```

`run` cannot be combined with `options`, `compiler`, `build`, `check`, `compilerCallback`, or `error`. The framework does not create a compiler or retain Stats for these cases; close owned compilers in `finally`. Both forms support `description`, `skip`, and an optional `name`. Names must be unique within the config and contain lowercase letters, digits, and single separating hyphens. Existing unnamed cases retain their index-based identifiers.

Use the full directory/scenario path to select a named case:

```bash
pnpm --dir tests/rspack-test run test -t 'compilerCases/object-lifecycle/hooks'
```

Import GC and compiler helpers from `@rspack/test-tools/helper/lifecycle`. GC scenarios run in the existing test process, which enables `--expose-gc`. For clean libuv measurements and natural-exit checks, use `runNodeCase(moduleURL, args)` from `@rspack/test-tools/helper/node-case`: the module default-exports an async function, receives JSON arguments, and returns a JSON result (or `null` for an empty return). Each child has a 20-second timeout; success requires natural process exit. See `compilerCases/libuv-handles` for examples.

### Defaults

| Entry File            | `Defaults.test.js`                                                                                        |
| --------------------- | --------------------------------------------------------------------------------------------------------- |
| Case Directory        | [`tests/defaultCases`](https://github.com/web-infra-dev/rspack/tree/main/tests/rspack-test/defaultCases)  |
| Output Directory      | None                                                                                                      |
| Default Configuration | [None](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/defaults.ts) |
| Run Output            | No                                                                                                        |

This test does not execute real builds; it only generates build configurations and observes the differences from the default configuration. The basic default configuration will be snapshot and stored in `tests/rspack-test/__snapshots__/Defaults.test.js.snap`.

This test uses `tests/rspack-test/fixtures` as the source code for the build, so the test case is written as a single file. Its structure is as follows:

```js title="{case}.js"
interface TDefaultsCaseConfig {
  description: string; // Description of the test case
  cwd?: string; // process.cwd for generating the build configuration of the test case, default is the `tests/rspack-test` directory
  options?: (context: ITestContext) => TCompilerOptions<ECompilerType.Rspack>; // Test case build configuration
  diff: (
    diff: Assertion<Diff>,
    defaults: Assertion<TCompilerOptions<ECompilerType.Rspack>>,
  ) => Promise<void>; // Differences from the default configuration
}

// [!code highlight:4]
/** @type {import('../..').TDefaultsCaseConfig} */
module.exports = {
  // ...
};
```

The details for the Error test are as follows:

### Error

| Entry File            | `Error.test.js`                                                                                                  |
| --------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Case Directory        | [`tests/errorCases`](https://github.com/web-infra-dev/rspack/tree/main/tests/rspack-test/errorCases)             |
| Output Directory      | None                                                                                                             |
| Default Configuration | [ErrorProcessor](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/error.ts) |
| Run Output            | No                                                                                                               |

This test uses `tests/rspack-test/fixtures` as the source code for the build, so the test case is written as a single file. Its structure is as follows:

```js title="{case}.js"
interface TErrorCaseConfig {
  description: string; // Description of the test case
  options?: (
    options: TCompilerOptions<T>,
    context: ITestContext,
  ) => TCompilerOptions<T>; // Test case configuration
  build?: (context: ITestContext, compiler: TCompiler<T>) => Promise<void>; // Test case build method
  check?: (stats: TStatsDiagnostics) => Promise<void>; // Function to check the test case
}

// [!code highlight:4]
/** @type {import('../..').TErrorCaseConfig} */
module.exports = {
  // ...
};
```

### Hook

| Entry File            | `Hook.test.js`                                                                                                 |
| --------------------- | -------------------------------------------------------------------------------------------------------------- |
| Case Directory        | [`tests/hookCases`](https://github.com/web-infra-dev/rspack/tree/main/tests/rspack-test/hookCases)             |
| Output Directory      | None                                                                                                           |
| Default Configuration | [HookProcessor](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/hook.ts) |
| Run Output            | No                                                                                                             |

This test records the input and output of the hook and stores it in the snapshot `hooks.snap.txt`. The snapshot of the final product code is stored in `output.snap.txt`.

This test uses `tests/rspack-test/fixtures` as the source code for the build, so the test case is written as a single file. Its structure is as follows:

```js title="{case}/test.js"
interface THookCaseConfig {
  description: string; // Description of the test case
  options?: (
    options: TCompilerOptions<T>,
    context: ITestContext,
  ) => TCompilerOptions<T>; // Test case configuration
  compiler?: (context: ITestContext, compiler: TCompiler<T>) => Promise<void>; // Callback after creating the compiler instance
  check?: (context: ITestContext) => Promise<void>; // Callback after the build is completed
}

// [!code highlight:4]
/** @type {import("@rspack/test-tools").THookCaseConfig} */
module.exports = {
  // ...
};
```

### TreeShaking

| Entry File            | `TreeShaking.test.js`                                                                                                        |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Case Directory        | `tests/treeShakingCases`                                                                                                     |
| Output Directory      | `tests/js/treeShaking`                                                                                                       |
| Default Configuration | [TreeShakingProcessor](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/treeshaking.ts) |
| Run Output            | No                                                                                                                           |

In this test case, the configuration is similar to a regular rspack project. You can specify the build configuration by adding a `rspack.config.js`, but the final product is snapshot and stored in `__snapshots__/treeshaking.snap.txt`.

### Builtin

| Entry File            | `Builtin.test.js`                                                                                                    |
| --------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Case Directory        | `tests/builtinCases`                                                                                                 |
| Output Directory      | `tests/js/builtin`                                                                                                   |
| Default Configuration | [BuiltinProcessor](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack-test-tools/src/case/builtin.ts) |
| Run Output            | No                                                                                                                   |

This test case is similar to a regular rspack project, and you can specify the build configuration by adding a `rspack.config.js`. However, depending on the directory, different snapshots of the products will be generated and stored in `__snapshots__/output.snap.txt`:

- **plugin-css**: Snapshots of files with a `.css` extension
- **plugin-css-modules**: Snapshots of files with `.css` and `.js` extensions
- **plugin-html**: Snapshots of files with `.html` extension
- **Other**: Snapshots of files with `.js` extension
