Vitest

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

Vitest 文档 展示了如何创建“Vitest 项目”配置,该配置从一个根命令运行 monorepo 中的所有测试,从而开箱即用地实现合并的代码覆盖率报告等功能。此功能不遵循 monorepo 的现代最佳实践,因为它旨在与 Jest 兼容(Jest 的 Workspace 功能是在 包管理器 Workspaces 之前构建的)。

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

因此,您有两种选择,每种都有其自身的权衡

利用 Turborepo 进行缓存

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

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

设置

假设我们有一个简单的 包管理器 Workspace,如下所示

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
    }
  }
}

您现在可以使用 全局 turboturbo run test:watch 的方式运行您的任务,或者从根目录的 package.json 中的脚本运行

终端
turbo run test
 
turbo run test:watch

创建合并的代码覆盖率报告

Vitest 的 Projects 功能 开箱即用地创建了代码覆盖率报告,该报告合并了您所有包的测试代码覆盖率报告。但是,按照 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 的 Projects 功能

Vitest Projects 功能不遵循与 包管理器 Workspace 相同的模型。相反,它使用一个根脚本,然后该脚本会触及仓库中的每个包,以处理相应包中的测试。

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

因此,如果您想使用 Turborepo 运行测试,您需要使用 Root Tasks。配置好 Vitest Projects 设置 后,为 Turborepo 创建 Root Tasks

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

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

使用混合方法

您可以通过实现混合解决方案来结合这两种方法的优点。此方法使用 Vitest 的 Projects 功能统一本地开发,同时保留 CI 中的 Turborepo 缓存。这需要进行一些额外的配置,并且在存储库中采用混合任务运行模型。

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

./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
  },
};

然后,使用 projects 创建您的根 Vitest 配置

./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 配置,这些配置导入共享配置。首先,安装共享配置包

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

然后创建 Vitest 配置

./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 以将新配置包包含在依赖关系图中

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 包含全局运行测试的脚本

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

此配置允许开发人员在根目录运行 pnpm test:projectspnpm test:projects:watch 以获得无缝的本地开发体验,使用 Vitest projects,而 CI 继续使用 turbo run test 来利用包级别的缓存。您仍需要手动处理合并的代码覆盖率报告,如上一节所述