close

Migrating from Vitest

If you are using the Rstack toolchain (Rsbuild / Rslib / Rspack, etc.), migrating to Rstest gives you a more consistent development experience.

Using Agent Skills

If you are using a Coding Agent that supports Skills, install the migrate-to-rstest skill to help with the upgrade process from Vitest to Rstest.

npx skills add rstackjs/agent-skills --skill migrate-to-rstest

After installation, let the Coding Agent guide you through the upgrade.

Install dependencies

First, you need to install the Rstest dependency.

npm
yarn
pnpm
bun
deno
npm add @rstest/core -D

Next, update the test script in your package.json to use rstest instead of vitest. For example:

"scripts": {
-  "test": "vitest run" // or "vitest --run"
+  "test": "rstest"
}

rstest does not have a --run flag. Running rstest already executes tests once and exits. If you want watch mode, use --watch:

"scripts": {
-  "test": "vitest"
+  "test": "rstest --watch"
}

Configuration migration

Update your Vitest configuration file (e.g., vite.config.ts or vitest.config.ts) to an rstest.config.ts file:

rstest.config.ts
import { defineConfig } from '@rstest/core';

export default defineConfig({
  include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'],
});

Test configuration

Rstest test configuration is basically the same as Vitest.

When migrating config, keep these changes in mind:

  • Remove the test field and move its nested properties to the top level.
  • Rename keys when required (for example, test.environment -> testEnvironment).

You can view all available test configuration options through Test Configurations.

import { defineConfig } from '@rstest/core';

export default defineConfig({
-  test: {
-    include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'],
-    exclude: ['dist/**'],
-    setupFiles: ['./test/setup.ts'],
-    globals: true,
-    environment: 'node',
-  },
+  include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'],
+  exclude: ['dist/**'],
+  setupFiles: ['./test/setup.ts'],
+  globals: true,
+  testEnvironment: 'node',
});

Build configuration

Rstest uses Rsbuild as the default test build tool instead of Vite. You can view all available build configuration options in Build Configurations.

In most projects, these are the key build-side changes:

  • Use source.define instead of define.
  • Use output.externals instead of ssr.external.
  • Use Rsbuild plugins instead of Vite plugins.
import { defineConfig } from '@rstest/core';
- import react from '@vitejs/plugin-react'
+ import { pluginReact } from '@rsbuild/plugin-react';

export default defineConfig({
-  plugins: [react()],
-  define: {
-    __DEV__: true,
-  },
+  plugins: [pluginReact()],
+  source: {
+    define: {
+      __DEV__: true,
+    },
+  },
});

If you are using Rslib or Rsbuild, you can directly use the corresponding adapter:

  • For Rslib projects (with rslib.config.*), use @rstest/adapter-rslib with extends: withRslibConfig() (see Rslib integration reference).
  • For Rsbuild projects (with rsbuild.config.*), use @rstest/adapter-rsbuild with extends: withRsbuildConfig() (see Rsbuild integration reference).

Replace test imports and APIs

For test APIs, migration is usually just two quick changes:

  1. Replace imports from vitest with imports from @rstest/core.
  2. Replace vi or vitest utility APIs with rs equivalents.
- import { describe, expect, it, test, vi, type Mock } from 'vitest';
+ import { describe, expect, it, test, rs, type Mock } from '@rstest/core';
- vi.fn()
+ rs.fn()

- vi.mock('./foo')
+ rs.mock('./foo')

- vi.spyOn(console, 'error')
+ rs.spyOn(console, 'error')
- vitest.fn()
+ rs.fn()

For the full utility API list, see Rstest APIs.

If you are using globals: true

When globals: true is enabled, vi and vitest are available as globals in Vitest. In Rstest, use this mapping order:

  • vi.<api> -> rs.<api>
  • vitest.<api> -> rstest.<api>

rs and rstest are equivalent global aliases, but keeping this one-to-one mapping is easier to read during migration.

- vi.fn()
+ rs.fn()

- vitest.spyOn(console, 'error')
+ rstest.spyOn(console, 'error')

If your tests import APIs from @rstest/core, prefer rs.<api> in import style and avoid mixing import style and global style in the same file.

Migrate setup adapters

Some setup adapters are Vitest-specific. For example, @testing-library/jest-dom/vitest is designed for Vitest and should be replaced in Rstest setup files.

- import '@testing-library/jest-dom/vitest';
+ import * as jestDomMatchers from '@testing-library/jest-dom/matchers';
+ import { expect } from '@rstest/core';
+
+ expect.extend(jestDomMatchers);

Path resolution in setup and test helpers

Depending on your transform/runtime mode, new URL(..., import.meta.url) may fail in setup or helper files.

If you see path errors such as Cannot find module './' or Cannot find module '..', prefer Node-style path resolution with __dirname:

- const root = fileURLToPath(new URL('../..', import.meta.url));
+ import { resolve } from 'node:path';
+ const root = resolve(__dirname, '../..');

Auto-mocking modules

In Vitest, calling vi.mock() with just the module path first attempts to load a manual mock from the corresponding __mocks__ directory. If no manual mock is found, it automatically mocks the module, replacing all its exports with empty mock functions.

// Vitest
import { vi, test, expect } from 'vitest';
import { someFunction } from './module';

// Looks for __mocks__/module.js first, then auto-mocks.
vi.mock('./module');

test('should be mocked', () => {
  expect(vi.isMockFunction(someFunction)).toBe(true);
  someFunction(); // returns undefined
});

Rstest handles this differently. Calling rs.mock() with only the module path will only look for a mock in the __mocks__ directory and will throw an error if one isn't found. To achieve auto-mocking, you must explicitly pass the { mock: true } option.

// Rstest
import { rs, test, expect } from '@rstest/core';
import { someFunction } from './module';

- // Looks for __mocks__/module.js first, then auto-mocks.
- vi.mock('./module');
+ // Auto-mocks the module because { mock: true } is passed.
+ rs.mock('./module', { mock: true });

test('should be mocked', () => {
  expect(rs.isMockFunction(someFunction)).toBe(true);
  someFunction(); // returns undefined
});

Mock async modules

When you need to mock a module's return value, Rstest does not support returning an async function.

As an alternative, Rstest provides synchronous importActual capability, allowing you to import the unmocked module implementation through static import statements:

import * as apiActual from './api' with { rstest: 'importActual' };

// Partially mock the './api' module
rs.mock('./api', () => ({
  ...apiActual,
  fetchUser: rs.fn().mockResolvedValue({ id: 'mocked' }),
}));

Because mock factories are hoisted, avoid relying on values initialized later in the same module. If needed, move shared values to a hoisted initializer (for example rs.hoisted(...)) to avoid initialization-order errors.