集成 Vitest

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

Vitest 文档展示了如何创建一个"Vitest 工作区",从一个根命令运行单体仓库中的所有测试,使得合并覆盖率报告等功能开箱即用。这个功能不遵循单体仓库的现代最佳实践,因为它的设计是为了与 Jest 兼容(其工作区功能是在包管理器工作区之前构建的)。

因此,你有两个选择,每个选择都有自己的权衡:

利用 Turborepo 进行缓存

为了提高缓存命中率并且只运行有变化的测试,你可以选择为每个包配置任务,将 Vitest 命令拆分成每个包中单独的、可缓存的脚本。这种速度带来的权衡是你需要自己创建合并的覆盖率报告。

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

设置

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

package.json
package.json

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

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

在根目录的 turbo.json 中,创建一个 test 任务:

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

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

在监视模式下运行测试

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

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

下面的策略创建了两个任务,一个用于本地开发,另一个用于 CI。你可以选择将 test 任务用于本地开发,而创建一个 test:ci 任务。

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

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

以及,在根目录的 turbo.json 中:

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

你现在可以使用全局 turbo 运行 turbo run test:watch,或者从根 package.json 中的脚本运行:

Terminal
turbo run test
 
turbo run test:watch

创建合并的覆盖率报告

Vitest 的工作区功能创建了一个开箱即用的覆盖率报告,合并了所有包的测试覆盖率报告。但是按照 Turborepo 策略,你将不得不自己合并覆盖率报告。

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

要做到这一点,你需要遵循几个一般步骤:

  1. 运行 turbo run test 创建覆盖率报告。
  2. 使用 nyc merge 合并覆盖率报告。
  3. 使用 nyc report 创建报告。

要完成的 Turborepo 任务看起来像:

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-vitest 示例 展示了一个完整的例子,你可以根据自己的需求进行调整。你可以使用 npx create-turbo@latest --example with-vitest 快速开始。

使用 Vitest 的工作区功能

Vitest 工作区功能不遵循与包管理器工作区相同的模型。相反,它使用一个根脚本,然后延伸到仓库中的每个包,处理该包中的测试。

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

因此,如果你想使用 Turborepo 运行测试,你需要使用根任务。一旦你配置了 Vitest 工作区,为 Turborepo 创建根任务:

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

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

使用混合方法

你可以通过实施混合解决方案来结合两种方法的优势。这种方法在保留 Turborepo 在 CI 中的缓存的同时,统一了使用 Vitest 工作区方法的本地开发。这带来的权衡是稍微更多的配置和仓库中的混合任务运行模型。

./vitest.workspace.ts
import { defineWorkspace } from 'vitest/config';
 
export default defineWorkspace(['packages/*']);

在这种设置中,你的包保持各自的 Vitest 配置和脚本:

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

而你的根 package.json 包含全局运行测试的脚本:

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

这种配置允许开发者在根目录运行 pnpm test:workspace:watch 以获得无缝的本地开发体验,而 CI 继续使用 turbo run test 来利用包级缓存。你仍然需要像上一节描述的那样手动处理合并的覆盖率报告