close
  • English
  • 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.

    Installation and setup

    First, you need to install Rstest as a development 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"
    }

    CLI option mappings

    Some Vitest CLI flags map directly to Rstest, while others change shape. Use this table for the common option differences you are likely to hit during migration:

    Vitest CLI optionRstest equivalentNotes
    vitest run / vitest --runrstestNo --run flag — rstest already runs once and exits.
    vitest / vitest watch / vitest --watchrstest --watch or rstest watchRstest does not auto-enable watch mode — pass --watch explicitly.
    vitest --coveragerstest --coverageAlso install the provider package that matches your config (see the coverage config row).
    vitest --environment=jsdomrstest --testEnvironment jsdom
    vitest --reporter=verboserstest --reporters verbose
    vitest --globalsrstest --globals
    vitest -t <pattern> / --testNamePatternrstest -t <pattern> / --testNamePattern
    vitest -u / --updaterstest -u / --update
    vitest -c <path> / --configrstest -c <path> / --config
    vitest --project <name>rstest --project <name>

    Configuration migration

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

    rstest.config.ts
    import { defineConfig } from '@rstest/core';
    
    export default defineConfig({
      // Fill in by mapping fields from your vitest.config.ts — see the table below.
    });

    Helper imports

    Vitest's config-file helpers map to equivalents in @rstest/core:

    Vitest (vitest/config)Rstest (@rstest/core)Notes
    defineConfigdefineConfig
    defineProjectdefineProjectFor items inside a projects array, prefer defineInlineProject — it requires an explicit name.
    defineWorkspaceRemoveNo workspace helper. Declare projects inline via the projects field on defineConfig — see Projects.
    mergeConfigmergeRstestConfigDeep-merges and preserves function-valued fields. For composing configs inside a projects array, use mergeProjectConfig.

    Vitest configuration mappings

    When migrating config, keep two changes in mind:

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

    Walk through every option under test and match it against the table below — move it to the top level, rename it, or drop it. Fields not listed here may not map 1:1; verify against the Rstest config reference before dropping them silently.

    Vitest (under test)Rstest (top-level)Notes
    environmenttestEnvironmentFold test.environmentOptions into the object form: testEnvironment: { name: 'jsdom', options: { ... } }. Custom environment packages are not supported.
    include / excludeinclude / exclude
    includeSourceincludeSource
    setupFilessetupFiles
    globalSetupglobalSetupRstest calls setup with no arguments — if your Vitest setup reads the TestProject argument (e.g. provide, onTestsRerun), rewrite without it. Vitest's provide / inject has no direct Rstest equivalent — mutate process.env in the setup (Rstest snapshots it post-setup into every worker) or set static values via the env config field.
    globalsglobals
    namename
    rootroot
    envenv
    aliasresolve.aliasNot a test.* field in Rstest — move aliases to top-level resolve.alias.
    passWithNoTestspassWithNoTests
    isolateisolate
    testTimeout / hookTimeouttestTimeout / hookTimeout
    teardownTimeoutRemoveNo equivalent — Vitest's teardownTimeout is a shutdown-wait timeout, unrelated to Rstest's hookTimeout (which covers lifecycle hooks).
    slowTestThresholdslowTestThreshold
    maxConcurrencymaxConcurrencyLimits test.concurrent / describe.concurrent cases inside one test file. To limit how many test files run at the same time, use pool.maxWorkers instead.
    fileParallelism: falsepool.maxWorkers: 1Rstest does not have a boolean fileParallelism switch. Set pool.maxWorkers to 1 to run test files one at a time.
    retryretry
    bailbail
    clearMocksclearMocks
    mockResetresetMocks
    restoreMocksrestoreMocks
    poolOptions.forks.maxForkspool.maxWorkersRstest flattens poolOptions into top-level pool: poolOptions.forks.maxForkspool.maxWorkers, execArgvpool.execArgv. minForks has no equivalent — Rstest does not expose a minWorkers knob. Only the forks pool is supported — pool: 'threads' | 'vmThreads' | 'vmForks' settings are dropped (behavior reverts to forks). Vitest 4's top-level test.maxWorkers maps to pool.maxWorkers (test.minWorkers is dropped).
    coveragecoverageRstest supports both provider: 'istanbul' and provider: 'v8'. Replace @vitest/coverage-v8 with @rstest/coverage-v8, or @vitest/coverage-istanbul with @rstest/coverage-istanbul. Rename coverage.reportercoverage.reporters (singular is silently ignored). These sub-keys map 1:1: include, exclude, reportsDirectory, thresholds. Vitest-specific keys without equivalents still need to be removed, such as all, skipFull, thresholdAutoUpdate, processingConcurrency, customProviderModule, watermarks, and ignoreClassMethods.
    reportersreportersVitest-only (drop or replace): tap, tap-flat, html, tree, hanging-process. String values must be built-in names — for third-party reporters, import the class and pass the instance.
    outputFilereporter optionsNo top-level field. For junit / json, pass outputPath via the reporter tuple: ['junit', { outputPath: '...' }]. For blob, use { outputDir: '...' }. Other reporters accept no output path. The object form { junit: 'a.xml', json: 'a.json' } expands to one tuple per reporter.
    snapshotFormatsnapshotFormat
    resolveSnapshotPathresolveSnapshotPathRstest's signature is (testPath, snapExtension) => string — no third context argument from Vitest 3+.
    snapshotSerializersexpect.addSnapshotSerializerNo config field. In a setupFiles module, import each serializer and call expect.addSnapshotSerializer(serializer).
    projectsprojects
    logHeapUsagelogHeapUsage
    detectAsyncLeaksdetectAsyncLeaksUse while debugging leaked async resources. Rstest reports active Node.js async resources after a test file finishes.
    includeTaskLocationincludeTaskLocation
    silentsilentMatches Vitest's boolean | 'passed-only' semantics for intercepted test console output.
    printConsoleTraceprintConsoleTrace
    unstubGlobalsunstubGlobals
    unstubEnvsunstubEnvs
    chaiConfigchaiConfig

    File-level environment comments

    If your Vitest tests use file-level environment comments, Rstest recognizes Vitest environment comments during migration:

    // @vitest-environment jsdom
    // @vitest-environment-options { "url": "https://example.com/" }

    You can keep these comments as-is or rename them to @rstest-environment / @rstest-environment-options. The options value must be a single-line JSON object. Supported environments are node, jsdom, and happy-dom.

    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.

    Rstest runs tests with a bundle-based execution model. Code outside the bundle graph keeps Node.js native loader behavior, which may differ from Vitest. If you dynamically load files at runtime, see Code outside the bundle graph uses native Node.js behavior.

    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).

    Update test API

    Test API

    Your existing Vitest test files should work with minimal changes since Rstest provides Vitest-compatible APIs. Update your imports from vitest to @rstest/core, and replace vi / vitest utilities with their rs / rstest 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.

    Global APIs

    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>rs.<api>

    rs and rstest are equivalent global aliases, but using rs keeps migrated examples consistent with import style.

    - vi.fn()
    + rs.fn()
    
    - vitest.spyOn(console, 'error')
    + rs.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.

    Setup adapters

    Some setup adapters are Vitest-specific. For example, @testing-library/jest-dom/vitest is designed for Vitest; in Rstest, register the matchers directly via expect.extend.

    - 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

    new URL('./file', import.meta.url) resolves relative to the source module, the same as in Node.js and Vitest, so it can locate sibling files (assets, fixtures) from setup or helper files:

    const fixtureUrl = new URL('./fixture.json', import.meta.url);

    Rstest keeps these expressions as-is instead of rewriting them into bundled asset paths, so the resolved URL points at the on-disk source file.

    Because these expressions stay out of the bundler's dependency graph, editing such a sibling file in watch mode does not automatically re-run the tests that read it at runtime.

    Auto-mocking modules

    Both Vitest and Rstest support auto-mocking when the mock call only includes the module path. Rstest 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 function exports with empty mock functions.

    This behavior is supported starting from Rstest 0.11.0. In earlier versions, pass { mock: true } explicitly to request auto-mocking when no manual mock exists.

    // Rstest
    import { rs, test, expect } from '@rstest/core';
    import { someFunction } from './module';
    
    // Looks for __mocks__/module.js first, then auto-mocks.
    rs.mock('./module');
    
    test('should be mocked', () => {
      expect(rs.isMockFunction(someFunction)).toBe(true);
      someFunction(); // returns undefined
    });

    If you want to skip the __mocks__ lookup and request auto-mocking directly, pass { mock: true } explicitly:

    rs.mock('./module', { mock: true });

    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.

    Snapshots

    Vitest and Rstest use the same snapshot key format and body serializer. Existing __snapshots__/*.snap files from a Vitest suite are read verbatim by Rstest, and tests that passed under Vitest continue to pass under Rstest without re-recording. Only the file header line differs:

    - // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
    + // Rstest Snapshot v1

    Running rstest -u normalizes this header to the Rstest form; the snapshot bodies stay byte-identical.