Multiple workspaces
Loading "Multiple Workspaces (🏁 solution)"
Run locally for transcripts
Real-world projects often combine multiple types of tests at the same time. Vitest is a great choice for both unit and integration tests, but those two have quite different requirements and, as a result, may need different setups.
Vitest solves this by using workspaces.
Vitest Workspaces
A workspace allows you to describe multiple configurations independently. Think of it as a package workspace, where a single repo can host multiple different packages. In the same way, a single Vitest process can run multiple different test projects.
Workspaces are listed in the
test.workspace
property in your Vitest config, and can include paths to nested packages or Vitest config files or inline configuration.I will define two workspaces:
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import { configDefaults } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
test: {
workspace: [
{
test: {
name: 'unit',
globals: true,
environment: 'node',
include: ['**/*.test.ts'],
exclude: [...configDefaults.exclude, '**/*.browser.test.ts(x)?'],
},
},
{
extends: true,
test: {
name: 'browser',
globals: true,
include: ['**/*.browser.test.ts(x)?'],
browser: {
enabled: true,
provider: 'playwright',
instances: [
{
browser: 'chromium',
setupFiles: ['./vitest.browser.setup.ts'],
},
],
},
},
},
],
},
})
Let's go over them in more detail.
The first workspace include the configuration for running unit tests:
{
test: {
name: 'unit',
globals: true,
environment: 'node',
include: ['**/*.test.ts'],
exclude: ['**/*.browser.test.ts(x)?'],
},
},
name
gives this workspace a name and allows me to run it in isolation anytime I want by providing the--project=unit
option to the Vitest CLI;include
lists the test file patterns to include in this run. In this case, I want for the unit tests to cover test files ending with*.test.ts
;exclude
does the opposite ofinclude
, listing the file patterns to ignore. Since my browser tests also end with*.test.ts
, I need to exclude them not to be confused with unit tests;environment
controls the test environment used for this workspace. I want my unit tests to run in Node.js, so I provide'node'
as the enivornment here.
🦉 Notice that each workspace lists Vitest configuration starting from the root by including thetest
key again. This is handy because each workspace can have a different set of Vite options andplugins
for different test files.
Similarly, here's the workspace for the browser (component) tests:
{
extends: true,
test: {
name: 'browser',
globals: true,
include: ['**/*.browser.test.ts(x)?'],
browser: {
enabled: true,
provider: 'playwright',
instances: [
{
browser: 'chromium',
setupFiles: ['./vitest.browser.setup.ts'],
},
],
},
},
},
Here, I'm naming this workspace
'browser'
and configuring it to include only *.browser.test.ts(x)
test files. These will be my component tests. For the rest of the configuration, I simply moved the existing test.browser
configuration under this workspace and left it as-is.TypeScript
The next step is to deal with TypeScript. One of the most overlooked aspects of using TypeScript is that you often need multiple configurations within the same project. Your source code, unit tests, integration tests, and test utilities are all written in TypeScript but have different concerns that may require different types.
📜 If you want to dive deeper into the reason behing using multiple TypeScript configurations and how to do that properly, read my post called One Thing Nobody Explained To You About TypeScript.
In our project, unit and component tests have a different set of type requirements because they run in different environments. This means I need to introduce two separate configurations to address these differences:
tsconfig.test.unit.json
and tsconfig.test.browser.json
.Let's start with the unit tests:
{
"extends": "./tsconfig.base.json",
"include": ["src/**/*.test.ts*"],
"exclude": ["src/**/*.browser.test.ts*"],
"compilerOptions": {
"types": ["node", "vitest/globals"]
}
}
Similar to how I've configured Vitest workspaces to apply only to specific file patterns, I tell TypeScript to apply this configuration only to the
*.test.ts
files, excluding the browser tests at *.browser.test.ts*
. The most important bit here is the compilerOptions.types
property:"types": ["node", "vitest/globals"]
This includes Node.js type definitions (
@types/node
) and Vitest global types (e.g. test
and expect
) for my unit tests. Since I don't have the Node.js types installed, I need to add them as a dependency to the project:npm i -D @types/node
Next, I rename the existing
tsconfig.test.json
to tsconfig.test.browser.json
to act as the TypeScript configuration for my browser component tests:{
"extends": "./tsconfig.app.json",
"include": [
"vitest.browser.setup.ts",
"src/**/*",
"src/**/*.browser.test.ts*"
],
"exclude": [],
"compilerOptions": {
"types": ["vitest/globals", "@vitest/browser/providers/playwright"]
}
}
Test commands
To make it easier to run specific types of tests, I will modify
package.json
to add test:unit
and test:integration
commands:{
"scripts": {
"test": "vitest",
"test:unit": "vitest --project=unit",
"test:integration": "vitest --project=browser"
}
}
Test files grouping
There are multiple ways to group different types of tests in the same project:
- By file name. This is the one I'm using this exercise, adopting
*.test.ts
for unit tests and*.browser.test.tsx
for integration tests; - By directory name. For example, you can keep unit tests in
./src
while integration tests in./tests
The choice is up to you — I only recommend you stick to one approach and don't mix them in your app.