Multiple workspaces

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 of include, 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 the test key again. This is handy because each workspace can have a different set of Vite options and plugins 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.

Please set the playground first

Loading "Multiple workspaces"
Loading "Multiple workspaces"

No tests here 😢 Sorry.