· testing / vitest / turborepo
Thiết lập Vitest trong monorepo Turborepo: hướng dẫn 2026
Vitest 4.x bỏ workspace, dùng projects. Hai cách cho Turborepo: per-package cache trong CI, root Vitest Projects cho local, turbo.json wiring và gộp coverage.
Bởi Ethan · Cập nhật 29 tháng 5, 2026
1.368 từ · 7 phút đọc
Thêm Vitest vào một monorepo Turborepo mất khoảng một tiếng. Phần lớn các hướng dẫn đều mắc một lỗi: Vitest 4.0 đã bỏ option workspace. Mọi bài viết ra đời trước tháng 10/2025 đều nhắc đến nó. Hãy dùng projects thay thế.
Hướng dẫn này trình bày hai cách mà các team thực sự sử dụng: cache theo từng package trong CI và một Vitest Projects root config để phát triển local. Repo ví dụ chính thức đi kèm hybrid của cả hai — bài này sẽ dẫn bạn đến đó.
Bài này dành cho ai
Các developer đang làm việc trên monorepo pnpm + Turborepo (hoặc đang thiết lập) và muốn tích hợp Vitest hoàn chỉnh. Bạn cần đã có một workspace chạy được với ít nhất một package có thể test. Nếu bắt đầu từ đầu, hãy đọc hướng dẫn cài pnpm + Turborepo trước, rồi quay lại đây. Nếu bạn đang phân vân giữa Vitest và Jest, Vitest vs Jest 2026 có so sánh hiệu năng chi tiết.
Hai cách tiếp cận
Trước khi chỉnh sửa bất cứ file nào, hãy hiểu mình đang chọn gì.
Cách A — vitest.config.ts theo từng package: mỗi package tự quản lý Vitest config và một script "test": "vitest run". Turborepo cache output test theo từng package. Khi thay đổi packages/ui, chỉ test của package đó được chạy lại. Coverage output tách biệt từng package và cần bước gộp thủ công. Đây là lựa chọn tốt hơn cho CI.
Cách B — Vitest Projects root: một file vitest.config.ts duy nhất ở root liệt kê tất cả package trong mảng projects. Vitest tự động gộp coverage. Bất kỳ thay đổi nào cũng làm bể toàn bộ cache vì Turborepo chỉ thấy một task test, không phải nhiều. Đây là lựa chọn tốt hơn để phát triển local.
Ví dụ chính thức with-vitest sử dụng hybrid: script theo Cách A trong package.json của mỗi package, nhưng vẫn có root config để dùng vitest --ui cho tiện. Bài này cũng làm tương tự.
Bước 1: Bootstrap từ ví dụ chính thức (tùy chọn)
Nếu bắt đầu repo mới, cách nhanh nhất là:
npx create-turbo@latest --example with-vitest my-monorepo
cd my-monorepo
pnpm install
Lệnh này tạo ra một điểm khởi đầu chạy được ngay. Phần còn lại của hướng dẫn giải thích nó đã cài gì — và cách tái tạo trong repo có sẵn.
Bước 2: Tạo shared Vitest config package
Một shared config package giúp vitest.config.ts của mọi package đều extends từ một nguồn duy nhất. Không có nó, cấu hình sẽ dần khác nhau giữa các package.
Tạo packages/vitest-config/ với cấu trúc sau:
packages/vitest-config/
├── package.json
├── tsconfig.json
└── src/
└── index.ts
packages/vitest-config/package.json:
{
"name": "@repo/vitest-config",
"version": "0.0.0",
"private": true,
"exports": {
".": "./src/index.ts"
},
"scripts": {
"build": "tsc --project tsconfig.json"
},
"devDependencies": {
"typescript": "latest",
"vitest": "^4.1.7"
}
}
packages/vitest-config/src/index.ts:
import { defineConfig } from 'vitest/config'
export const defineSharedConfig = () =>
defineConfig({
test: {
environment: 'node',
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
},
},
})
packages/vitest-config/tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"declaration": true,
"outDir": "./dist"
},
"include": ["src"]
}
Thêm package vào pnpm-workspace.yaml ở workspace root nếu chưa có:
packages:
- 'apps/*'
- 'packages/*'
Lưu ý quan trọng: coverage và reporters không thể cấu hình theo từng project trong Vitest 4.x — chúng phải ở root config. Đặt chúng trong shared config và đừng cố override theo từng package.
Bước 3: vitest.config.ts theo từng package
Với mỗi package có test, thêm hai thứ sau.
packages/ui/package.json (thêm vào scripts và devDependencies):
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
},
"devDependencies": {
"@repo/vitest-config": "workspace:*",
"vitest": "^4.1.7"
}
}
packages/ui/vitest.config.ts:
import { defineSharedConfig } from '@repo/vitest-config'
export default defineSharedConfig()
Quan trọng: script phải là vitest run, không phải vitest. Chạy vitest không có đối số sẽ khởi động watch mode — không bao giờ kết thúc. Turborepo sẽ chờ mãi, và cache hit rate sẽ bằng không vì một task treo không bao giờ ghi output.
Bước 4: Kết nối Turborepo
turbo.json ở repo root cần một task test phụ thuộc vào việc shared config được build trước.
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["^build", "@repo/vitest-config#build"],
"outputs": ["coverage/**"],
"cache": true
},
"test:watch": {
"cache": false,
"persistent": true
}
}
}
Dependency @repo/vitest-config#build đảm bảo shared config được compile trước khi bất kỳ package nào import nó. Thiếu bước này sẽ gây lỗi import trên cold install.
Task test:watch đặt "cache": false, "persistent": true. Watch task không bao giờ kết thúc — đánh dấu persistent ngăn Turborepo coi chúng là failed, còn tắt cache ngăn Turborepo cố ghi output không bao giờ có.
Bước 5: Chạy test từ root
Sau khi cấu hình turbo.json, chạy tất cả test từ repo root:
pnpm turbo test
Lần chạy đầu tiên: tất cả package thực thi. Lần chạy tiếp theo không có thay đổi: tất cả package được cache — output hiện ra ngay lập tức.
Tasks: 6 successful, 6 total
Cached: 6 cached, 6 total
Time: 312ms >>> FULL TURBO
Chạy test cho một package cụ thể:
pnpm turbo test --filter=@repo/ui
Watch mode để phát triển:
pnpm turbo test:watch --filter=@repo/ui
Bước 6: Root Vitest Projects config (tùy chọn — tiện cho local)
Một file vitest.config.ts ở root cho phép chạy tất cả test trong một process duy nhất và nhận merged coverage mà không cần bước gộp riêng. Đây là thứ ví dụ chính thức đi kèm để dùng local.
vitest.config.ts ở repo root:
import { defineConfig } from 'vitest/config'
import { glob } from 'glob'
import path from 'path'
const packages = glob.sync('packages/*/vitest.config.ts').map((config) =>
path.resolve(config)
)
export default defineConfig({
test: {
projects: packages,
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
},
},
})
Chạy vitest ở root với config này sẽ tự khám phá config của mỗi package qua mảng projects. Coverage được gộp tự động. Đánh đổi: một lần test chạy toàn bộ, Turborepo cache thành một đơn vị. Bất kỳ thay đổi file nào cũng chạy lại mọi test. Chấp nhận được khi làm local. Trong CI, dùng turbo test theo package thay thế.
Bước 7: Gộp coverage trong CI
Nếu dùng Cách A (cache theo package) và muốn có merged coverage report, cần thêm bước sau turbo test.
Mỗi package ghi coverage vào packages/<name>/coverage/ riêng. Gộp chúng bằng nyc:
pnpm add -Dw nyc
# Sau turbo test:
nyc merge packages/*/coverage/coverage-final.json coverage/merged.json
nyc report --reporter=text --reporter=lcov --temp-dir coverage
Thêm bước này vào CI sau step turbo test. Đừng thêm vào turbo.json — nó cần output của tất cả package tồn tại trước, và chạy đủ nhanh để không cần cache.
Remote caching cho CI phân tán
Cache mà Turborepo tạo local vô hình với các máy khác trừ khi bật remote caching. Vercel Remote Cache miễn phí cho mọi gói — kết nối repo và cache tự động đồng bộ giữa các máy:
npx turbo login
npx turbo link
Sau khi liên kết, một CI runner không có local state chung với máy dev của bạn vẫn nhận cache hit cho các package không thay đổi. Với monorepo 8–10 package, cache hit trên các path không thay đổi hoàn thành trong vài giây thay vì vài phút.
Tổng hợp các lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
vitest bị treo trong Turborepo | watch mode trong script test | Đổi sang vitest run |
Cannot find module '@repo/vitest-config' | shared config chưa được build | Thêm @repo/vitest-config#build vào dependsOn |
| Coverage config bị bỏ qua theo package | Giới hạn của Vitest 4 | Đặt coverage trong shared root config |
Option workspace không được nhận diện | Đã bị xóa trong Vitest 4.0 | Dùng projects |
| CI cache miss mỗi lần chạy | test:watch có cache: true | Đặt cache: false, persistent: true |