I am a huge fan of Vitest. I use Jest in some projects (mostly for legacy reasons) but for all new projects Vitest is my default test runner.
I have to admit, I've always maintained multiple configuration files. I even wrote a blog post about how to set up Vitest Browser Mode and suggested using two different config files.
Thanks to a PR and feedback from someone on the Vitest team (Vladimir ) I am completely converted - Vitest projects are so much easier to maintain and they're so easy to set up too!
What are Vitest projects?
Within your main Vitest config, a project is a separate test runner configuration, that shares a base config but overrides config specific to that project.
(Your Vitest config can have multiple projects. Typically you'd use it to run different test files/directories with different configuration/environment.)
What you can use projects for in Vitest
If you have different sets of tests, then you can put each type in its own project. Then you can run just those tests.
For example:
- split up your tests between faster
unitand slowerintegrationtests - split up your Vitest Browser-Mode tests and your regular (non Browser-mode tests)
- separate frontend component tests from backend API tests
- different projects that require different environments (e.g. node, happy-dom, jsdom)
- separate projects that require different global setup/teardown logic
Why use projects instead of multiple config files?
- Very easy to setup, just one file
- Simple way to share duplicated config between different projects
- They are very easy to run with
--project your_project_name. - You can also run tests from multiple projects (like
yarn vitest --project proj1 --project proj2) at the same time. Something that is not possible with multiple configs
How to set them up
Let's say you have two sets of tests - unit and integration tests.
In this blog post I will assume they either have the file ending *.unit.test.ts and *.integration.test.ts (or .tsx)
We will have two projects. Let's update our package.json
{
"scripts": {
"test:unit": "vitest --project unit",
"test:integration": "vitest --project integration"
}
}
And in your Vitest config file:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
setupFiles: ['./vitest.setup.ts'],
environment: 'jsdom',
projects: [
{
extends: true,
test: {
name: 'unit', // optional, but it makes it easier to give each project a unique name
include: [
'**/*.unit.test.{ts,tsx}',
],
},
},
{
extends: true,
test: {
name: 'integration',
include: [
'**/*.integration.test.{ts,tsx}',
],
},
},
],
},
});
And that is all you need to do.
More info on extends: true later on,
but this just means that the project
inherits the config from the top level
test config
How it was done with separate vitest config files (Show details)
You might be reading this and wondering how you can do the same thing, but with two different vitest config files.
It is as simple as these two steps:
- having two files (e.g.
vitest.unit.config.tsandvitest.integration.config.ts) - then using the config file option when running vitest - like
vitest --config=vitest.unit.config.ts
How to use Vitest projects with Vitest Browser Mode
It works in a very similar way - just pass in the browser config options to your browser project.
In the next example we set browser config, and have different setupFiles:
import { defineConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
export default defineConfig({
test: {
globals: true,
projects: [
{
extends: true,
test: {
name: 'unit',
setupFiles: [
'./vitest.setup.ts',
],
include: [
'**/*.test.{ts,tsx,js,jsx}',
],
environment: 'jsdom',
},
},
// the project with browser mode config:
{
extends: true,
test: {
name: 'browser',
setupFiles: [
'./vitest.browser.setup.ts',
],
include: [
'./**/*.browser.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
],
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
},
},
},
],
},
});
You can see a working version in this starter Vitest Browser Mode github repo
How to select different tests for your different projects
Option 1 - file based suffixes:
Using file based suffixes is the easiest way for most projects.
For example *.integration.test.tsx and *.unit.test.tsx
But if you already have all your tests with *.test.tsx, it isn't as straightforward.
You can however combine it with the exclude:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
projects: [
{
test: {
name: 'unit',
include: [
'**/*.test.{ts,tsx}',
],
exclude: [
'**/*.integration.test.{ts,tsx}',
], // << exclude *integration.test.ts here
},
},
{
test: {
name: 'integration',
include: [
'**/*.integration.test.{ts,tsx}',
],
},
},
],
},
});
Option 2 - different directories
Depending on your app and file structure, using different directories can also work and be very straightforward to set up.
A huge downside to this is that it is
often beneficial to colocate your
tests next to the implementation -
otherwise it isn't easy to find and
isn't clear if a test exists for a
file if it is located in a directory
somewhere else in the repo. In the
examples on this page though we avoid
that being a huge issue by always
putting it in ./__tests__/unit/ or
./__tests__/integration/.
export default defineConfig({
test: {
projects: [
{
test: {
name: 'unit',
include: [
'src/**/__tests__/unit/**/*.test.{ts,tsx}',
],
},
},
{
test: {
name: 'integration',
include: [
'src/**/__tests__/integration/**/*.test.{ts,tsx}',
],
},
},
],
},
});
How to reduce duplication between projects
In the examples on this page it always uses extends: true, which means any shared configuration between projects is put in the root config option.
In the following example, the globals and environment are used in both of the 2 projects, because of extends: true.
export default defineConfig({
test: {
globals: true, // << extends to each project
environment: 'jsdom', // << extends to each project
projects: [
{
extends: true, // << because of this
test: {
name: 'unit',
include: [
'**/*.unit.test.ts',
],
},
},
{
extends: true, // << because of this
test: {
name: 'integration',
include: [
'**/*.integration.test.ts',
],
},
},
],
},
});
Should you use Vitest projects if you have only one project?
There is no practical advantage - you might as well wait until you need to add another project.
If you have a simple app, no need to overcomplicate it if there is no benefit.
What you cannot put into project configurations
You have to put coverage and reporters into the root config. If you need separate configuration for these, then you will have to use separate configuration files
How to run all projects
You can run all projects by just omitting any --project flag when calling the vitest command.
Examples of Vitest projects
- Vitest Browser Mode starter repo which uses projects for Browser mode vs not Browser mode
- More examples