Vitest

Vitest is a test runner from the Vite ecosystem. Integrating it with Turborepo will lead to enormous speed-ups.

The Vitest documentation shows how to create a "Vitest Workspace" that runs all tests in the monorepo from one root command, enabling behavior like merged coverage reports out-of-the-box. This feature doesn't follow modern best practices for monorepos, since its designed for compatibility with Jest (whose Workspace feature was built before package manager Workspaces).

Because of this you have two options, each with their own tradeoffs:

Leveraging Turborepo for caching

To improve on cache hit rates and only run tests with changes, you can choose to configure tasks per-package, splitting up the Vitest command into separate, cacheable scripts in each package. This speed comes with the tradeoff that you'll need to create merged coverage reports yourself.

For a complete example, run npx create-turbo@latest --example with-vitest or visit the example's source code.

Setting up

Let's say we have a simple package manager Workspace that looks like this:

package.json
package.json

Both apps/web and packages/ui have their own test suites, with vitest installed into the packages that use them. Their package.json files include a test script that runs Vitest:

./apps/web/package.json
{
  "scripts": {
    "test": "vitest run"
  },
  "devDependencies": {
    "vitest": "latest"
  }
}

Inside the root turbo.json, create a test task:

Turborepo logo
./turbo.json
{
  "tasks": {
    "test": {
      "dependsOn": ["transit"]
    },
    "transit": {
      "dependsOn": ["^transit"]
    }
  }
}

Now, turbo run test can parallelize and cache all of the test suites from each package, only testing code that has changed.

Running tests in watch mode

When you run your test suite in CI, it logs results and eventually exits upon completion. This means you can cache it with Turborepo. But when you run your tests using Vitest's watch mode during development, the process never exits. This makes a watch task more like a long-running, development task.

Because of this difference, we recommend specifying two separate Turborepo tasks: one for running your tests, and one for running them in watch mode.

This strategy below creates two tasks, one for local development and one for CI. You could choose to make the test task for local development and create some test:ci task instead.

For example, inside the package.json file for each workspace:

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

And, inside the root turbo.json:

Turborepo logo
./turbo.json
{
  "tasks": {
    "test": {
      "dependsOn": ["^test"]
    },
    "test:watch": {
      "cache": false,
      "persistent": true
    }
  }
}

You can now run your tasks using global turbo as turbo run test:watch or from a script in your root package.json:

Terminal
turbo run test
 
turbo run test:watch

Creating merged coverage reports

Vitest's Workspace feature creates an out-of-the-box coverage report that merges all of your packages' tests coverage reports. Following the Turborepo strategy, though, you'll have to merge the coverage reports yourself.

The with-vitest example shows a complete example that you may adapt for your needs. You can get started with it quickly using npx create-turbo@latest --example with-vitest.

To do this, you'll follow a few general steps:

  1. Run turbo run test to create the coverage reports.
  2. Merge the coverage reports with nyc merge.
  3. Create a report using nyc report.

Turborepo tasks to accomplish will look like:

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/**"]
    },
  }
}

With this in place, run turbo test && turbo report to create a merged coverage report.

The with-vitest example shows a complete example that you may adapt for your needs. You can get started with it quickly using npx create-turbo@latest --example with-vitest.

Using Vitest's Workspace feature

The Vitest Workspace feature doesn't follow the same model as a package manager Workspace. Instead, it uses a root script that then reaches out into each package in the repository to handle the tests in that respective package.

In this model, there aren't package boundaries, from a modern JavaScript ecosystem perspective. This means you can't rely on Turborepo's caching, since Turborepo leans on those package boundaries.

Because of this, you'll need to use Root Tasks if you want to run the tests using Turborepo. Once you've configured a Vitest Workspace, create the Root Tasks for Turborepo:

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

Notably, the file inputs for a Root Task include all packages by default, so any change in any package will result in a cache miss. While this does make for a simplified configuration to create merged coverage reports, you'll be missing out on opportunities to cache repeated work.

Using a hybrid approach

You can combine the benefits of both approaches by implementing a hybrid solution.This approach unifies local development using Vitest's Workspace approach while preserving Turborepo's caching in CI. This comes at the tradeoff of slightly more configuration and a mixed task running model in the repository.

./vitest.workspace.ts
import { defineWorkspace } from 'vitest/config';
 
export default defineWorkspace(['packages/*']);

In this setup, your packages maintain their individual Vitest configurations and scripts:

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

While your root package.json includes scripts for running tests globally:

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

This configuration allows developers to run pnpm test:workspace:watch at the root for a seamless local development experience, while CI continues to use turbo run test to leverage package-level caching. You'll still need to handle merged coverage reports manually as described in the previous section.