Vitest

Vitest 是来自 Vite 生态系统的测试运行器。将其与 Turborepo 集成将带来巨大的速度提升。

¥Vitest is a test runner from the Vite ecosystem. Integrating it with Turborepo will lead to enormous speed-ups.

Vitest 文档 展示了如何创建一个 "Vitest 项目" 配置,该配置可以通过一个根命令运行 monorepo 中的所有测试,从而实现开箱即用的合并覆盖率报告等功能。此功能不符合 monorepo 的现代最佳实践,因为它的设计初衷是与 Jest 兼容(Jest 的工作区功能是在 软件包管理器工作区 之前构建的)。

¥The Vitest documentation shows how to create a "Vitest Projects" configuration that runs all tests in the monorepo from one root command, enabling behavior like merged coverage reports out-of-the-box. This feature doesn't follow modern best practices for monorepos, since its designed for compatibility with Jest (whose Workspace feature was built before package manager Workspaces).

Vitest 已弃用工作区,转而使用项目。使用项目时,单个项目的 vitest 配置无法再扩展根配置,因为它们会继承项目的配置。相反,需要一个像 vitest.shared.ts 这样的单独共享文件。

¥Vitest has deprecated workspaces in favor of projects. When using projects, individual project vitest configs can't extend the root config anymore since they would inherit the projects configuration. Instead, a separate shared file like vitest.shared.ts is needed.

因此,你有两个选项,每个选项都有各自的优缺点:

¥Because of this you have two options, each with their own tradeoffs:

利用 Turborepo 进行缓存

¥Leveraging Turborepo for caching

为了提高缓存命中率并仅运行包含更改的测试,你可以选择为每个包配置任务,将 Vitest 命令拆分为每个包中单独的可缓存脚本。这种速度的代价是你需要自己创建合并的覆盖率报告。

¥To improve on cache hit rates and only run tests with changes, you can choose to configure tasks per-package, splitting up the Vitest command into separate, cacheable scripts in each package. This speed comes with the tradeoff that you'll need to create merged coverage reports yourself.

完整示例:运行 npx create-turbo@latest --example with-vitest访问示例源代码

¥For a complete example, run npx create-turbo@latest --example with-vitest or visit the example's source code.

设置

¥Setting up

假设我们有一个简单的 软件包管理器工作区,如下所示:

¥Let's say we have a simple package manager Workspace that looks like this:

package.json
package.json

apps/webpackages/ui 都有自己的测试套件,vitest安装到使用它们的包中 分别是测试套件。他们的 package.json 文件包含一个运行 Vitest 的 test 脚本:

¥Both apps/web and packages/ui have their own test suites, with vitest installed into the packages that use them. Their package.json files include a test script that runs Vitest:

./apps/web/package.json
{
  "scripts": {
    "test": "vitest run"
  },
  "devDependencies": {
    "vitest": "latest"
  }
}

在根 turbo.json 文件中,创建一个 test 任务:

¥Inside the root turbo.json, create a test task:

Turborepo logo
./turbo.json
{
  "tasks": {
    "test": {
      "dependsOn": ["transit"]
    },
    "transit": {
      "dependsOn": ["^transit"]
    }
  }
}

现在,turbo run test 可以并行化和缓存每个包中的所有测试套件,只测试已更改的代码。

¥Now, turbo run test can parallelize and cache all of the test suites from each package, only testing code that has changed.

在监视模式下运行测试

¥Running tests in watch mode

当你在 CI 中运行测试套件时,它会记录结果并在完成后最终退出。这意味着你可以进行 使用 Turborepo 缓存。但是,当你在开发过程中使用 Vitest 的监视模式运行测试时,该进程永远不会退出。这使得监视任务更像 长期运行的开发任务

¥When you run your test suite in CI, it logs results and eventually exits upon completion. This means you can cache it with Turborepo. But when you run your tests using Vitest's watch mode during development, the process never exits. This makes a watch task more like a long-running, development task.

由于这种差异,我们建议指定两个单独的 Turborepo 任务:一个用于运行测试,另一个用于在监视模式下运行测试。

¥Because of this difference, we recommend specifying two separate Turborepo tasks: one for running your tests, and one for running them in watch mode.

以下策略创建了两个任务,一个用于本地开发,一个用于持续集成 (CI)。你可以选择将 test 任务用于本地开发,并创建一些 test:ci 任务。

¥This strategy below creates two tasks, one for local development and one for CI. You could choose to make the test task for local development and create some test:ci task instead.

例如,在每个工作区的 package.json 文件中:

¥For example, inside the package.json file for each workspace:

./apps/web/package.json
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest --watch"
  }
}

并且,在根 turbo.json 中:

¥And, inside the root turbo.json:

Turborepo logo
./turbo.json
{
  "tasks": {
    "test": {
      "dependsOn": ["^test"]
    },
    "test:watch": {
      "cache": false,
      "persistent": true
    }
  }
}

你现在可以将 全局 turbo 用作 turbo run test:watch 或从根目录 package.json 中的脚本运行任务:

¥You can now run your tasks using global turbo as turbo run test:watch or from a script in your root package.json:

Terminal
turbo run test
 
turbo run test:watch

创建合并覆盖率报告

¥Creating merged coverage reports

Vitest 的项目功能 会创建一个开箱即用的覆盖率报告,该报告会合并你所有软件包的测试覆盖率报告。但是,按照 Turborepo 的策略,你必须自行合并覆盖率报告。

¥Vitest's Projects feature creates an out-of-the-box coverage report that merges all of your packages' tests coverage reports. Following the Turborepo strategy, though, you'll have to merge the coverage reports yourself.

with-vitest 示例 展示了一个完整的示例,你可以根据自己的需求进行调整。你可以使用 npx create-turbo@latest --example with-vitest 快速上手。

¥The with-vitest example shows a complete example that you may adapt for your needs. You can get started with it quickly using npx create-turbo@latest --example with-vitest.

为此,你需要遵循几个常规步骤:

¥To do this, you'll follow a few general steps:

  1. 运行 turbo run test 以创建覆盖率报告。

    ¥Run turbo run test to create the coverage reports.

  2. 将覆盖率报告与 nyc merge 合并。

    ¥Merge the coverage reports with nyc merge.

  3. 使用 nyc report 创建报告。

    ¥Create a report using nyc report.

Turborepo 要完成的任务如下所示:

¥Turborepo tasks to accomplish will look like:

Turborepo logo
./turbo.json
{
  "tasks": {
    "test": {
      "dependsOn": ["^test", "@repo/vitest-config#build"],
      "outputs": ["coverage.json"]
    }
    "merge-json-reports": {
      "inputs": ["coverage/raw/**"],
      "outputs": ["coverage/merged/**"]
    },
    "report": {
      "dependsOn": ["merge-json-reports"],
      "inputs": ["coverage/merge"],
      "outputs": ["coverage/report/**"]
    },
  }
}

完成此配置后,运行 turbo test && turbo report 以创建合并的覆盖率报告。

¥With this in place, run turbo test && turbo report to create a merged coverage report.

with-vitest 示例 展示了一个完整的示例,你可以根据自己的需求进行调整。你可以使用 npx create-turbo@latest --example with-vitest 快速上手。

¥The with-vitest example shows a complete example that you may adapt for your needs. You can get started with it quickly using npx create-turbo@latest --example with-vitest.

使用 Vitest 的项目功能

¥Using Vitest's Projects feature

Vitest 项目功能与 软件包管理器工作区 的模型不同。相反,它使用一个根脚本,然后深入到存储库中的每个软件包中,处理相应软件包中的测试。

¥The Vitest Projects feature doesn't follow the same model as a package manager Workspace. Instead, it uses a root script that then reaches out into each package in the repository to handle the tests in that respective package.

从现代 JavaScript 生态系统的角度来看,此模型中不存在包边界。这意味着你不能依赖 Turborepo 的缓存,因为 Turborepo 依赖于这些包边界。

¥In this model, there aren't package boundaries, from a modern JavaScript ecosystem perspective. This means you can't rely on Turborepo's caching, since Turborepo leans on those package boundaries.

因此,如果你想使用 Turborepo 运行测试,则需要使用 根任务。配置好 Vitest 项目设置 后,为 Turborepo 创建根任务:

¥Because of this, you'll need to use Root Tasks if you want to run the tests using Turborepo. Once you've configured a Vitest Projects setup, create the Root Tasks for Turborepo:

Turborepo logo
./turbo.json
{
  "tasks": {
    "//#test": {
      "outputs": ["coverage/**"]
    },
    "//#test:watch": {
      "cache": false,
      "persistent": true
    }
  }
}

需要注意的是,根任务的文件输入默认包含所有包,因此任何包中的任何更改都会导致缓存未命中。虽然这确实简化了创建合并覆盖率报告的配置,但你将错失缓存重复工作的机会。

¥Notably, the file inputs for a Root Task include all packages by default, so any change in any package will result in a cache miss. While this does make for a simplified configuration to create merged coverage reports, you'll be missing out on opportunities to cache repeated work.

使用混合方法

¥Using a hybrid approach

你可以通过实现混合解决方案来结合两种方法的优势。此方法使用 Vitest 的项目功能统一本地开发,同时在 CI 中保留 Turborepo 的缓存。这需要付出一些代价,即配置略多,并且存储库中的任务运行模型会比较复杂。

¥You can combine the benefits of both approaches by implementing a hybrid solution. This approach unifies local development using Vitest's Projects feature while preserving Turborepo's caching in CI. This comes at the tradeoff of slightly more configuration and a mixed task running model in the repository.

首先,创建一个共享配置包,因为在使用项目时,单个项目无法扩展根配置。为你的共享 Vitest 配置创建一个新包:

¥First, create a shared configuration package since individual projects can't extend the root config when using projects. Create a new package for your shared Vitest configuration:

./packages/vitest-config/package.json
{
  "name": "@repo/vitest-config",
  "version": "0.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch"
  },
  "dependencies": {
    "vitest": "latest"
  },
  "devDependencies": {
    "@repo/typescript-config": "workspace:*",
    "typescript": "latest"
  }
}
./packages/vitest-config/tsconfig.json
{
  "extends": "@repo/typescript-config/base.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"],
  "exclude": ["dist", "node_modules"]
}
./packages/vitest-config/src/index.ts
export const sharedConfig = {
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    // Other shared configuration
  }
};

然后,使用项目创建你的 Vitest 根配置:

¥Then, create your root Vitest configuration using projects:

./vitest.config.ts
import { defineConfig } from 'vitest/config';
import { sharedConfig } from '@repo/vitest-config';
 
export default defineConfig({
  ...sharedConfig,
  projects: [
    {
      name: 'packages',
      root: './packages/*',
      test: {
        ...sharedConfig.test,
        // Project-specific configuration
      }
    }
  ]
});

在此设置中,你的包将维护各自的 Vitest 配置,并导入共享配置。首先,安装共享配置包:

¥In this setup, your packages maintain their individual Vitest configurations that import the shared config. First, install the shared config package:

./packages/ui/package.json
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest --watch"
  },
  "devDependencies": {
    "@repo/vitest-config": "workspace:*",
    "vitest": "latest"
  }
}

然后创建 Vitest 配置:

¥Then create the Vitest configuration:

./packages/ui/vitest.config.ts
import { defineConfig } from 'vitest/config';
import { sharedConfig } from '@repo/vitest-config';
 
export default defineConfig({
  ...sharedConfig,
  test: {
    ...sharedConfig.test,
    // Package-specific overrides if needed
  }
});

确保更新 turbo.json,将新的配置包包含在依赖图中:

¥Make sure to update your turbo.json to include the new configuration package in the dependency graph:

Turborepo logo
./turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["^test", "@repo/vitest-config#build"]
    },
    "test:watch": {
      "cache": false,
      "persistent": true
    }
  }
}

虽然你的根 package.json 包含用于全局运行测试的脚本:

¥While your root package.json includes scripts for running tests globally:

./package.json
{
  "scripts": {
    "test:projects": "vitest run",
    "test:projects:watch": "vitest --watch"
  }
}

此配置允许开发者在根目录下运行 pnpm test:projectspnpm test:projects:watch,以便在使用 Vitest 项目时获得无缝的本地开发体验,而 CI 则继续使用 turbo run test 来利用包级缓存。你仍然需要按照上一节中的说明手动处理合并的覆盖率报告。

¥This configuration allows developers to run pnpm test:projects or pnpm test:projects:watch at the root for a seamless local development experience using Vitest projects, while CI continues to use turbo run test to leverage package-level caching. You'll still need to handle merged coverage reports manually as described in the previous section.