---
title: "Configure Turborepo for Tests"
description: "Add test task to turbo.json, configure dependencies, configure outputs, and run turbo run test."
canonical_url: "https://vercel.com/academy/production-monorepos/configure-turborepo-tests"
md_url: "https://vercel.com/academy/production-monorepos/configure-turborepo-tests.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T23:11:58.760Z"
content_type: "lesson"
course: "production-monorepos"
course_title: "Production Monorepos with Turborepo"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Configure Turborepo for Tests

# Configure Turborepo for tests

You can run tests in packages/ui, but to run tests across the entire monorepo you need to execute `pnpm --filter` multiple times. As you add more packages with tests, this becomes tedious. You need a single command to test everything in parallel with intelligent caching.

Turborepo already orchestrates build and lint tasks. You'll add test as a pipeline task so Turborepo runs tests in dependency order, caches results, and only re-runs tests when code changes.

## Outcome

Add test task to turbo.json and run tests across the monorepo with caching and parallelization.

## Fast track

1. Add test task to turbo.json
2. Add test scripts to package.json files
3. Run `turbo test` across workspace
4. Verify caching works for tests

## Hands-on exercise 5.3

Configure Turborepo to run tests across all packages.

**Requirements:**

1. Add test task to turbo.json with:
   - No dependsOn (tests don't depend on other tasks)
   - outputs: \['coverage/\*\*'] to cache coverage reports
2. Add test script to root package.json: `turbo test`
3. Verify packages/ui has test script
4. Run `turbo test` to execute all tests
5. Run again to see cache hits
6. Understand when tests run and when they're cached

**Implementation hints:**

- Test task doesn't need `^test` dependency (no workspace dependencies)
- Coverage output should be cached if you generate coverage reports
- Tests are cached based on source file changes
- Use --force to bypass cache

## Add test task to turbo.json

Open `turbo.json` and add the test task:

```json title="turbo.json" {11-13}
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "test": {
      "outputs": ["coverage/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}
```

**What this means:**

- **No dependsOn** - Tests run independently, don't wait for build/lint
- **outputs: \["coverage/**"]\*\* - Cache coverage reports (if generated)
- **cache: true** (default) - Cache test results

**Why no `^test`?**
Unlike build and lint, tests don't have cross-package dependencies. You don't need to test packages/ui before testing apps/snippet-manager - they can run in parallel.

## Add test script to root

Update root `package.json`:

```json title="package.json" {5}
{
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "lint": "turbo lint",
    "test": "turbo test"
  }
}
```

Now you can run `pnpm test` from root to test all packages.

## Verify package scripts

Check that packages/ui has a test script (you added it in lesson 4.1):

```json title="packages/ui/package.json"
{
  "scripts": {
    "lint": "eslint .",
    "test": "vitest run",
    "dev:test": "vitest"
  }
}
```

Good! packages/ui is ready for Turborepo orchestration.

**Future packages:**
When you add more packages (`packages/utils`, `packages/typescript-config`, `packages/eslint-config`), add test scripts there too if they have tests. Note: config packages typically don't have tests since they're just static configuration files.

## Try it

### 1. Run tests across monorepo

```bash
turbo test
```

Output:

```
• Packages in scope: @geniusgarage/ui, @geniusgarage/web, @geniusgarage/snippet-manager, @geniusgarage/utils
• Running test in 4 packages

@geniusgarage/ui:test: cache miss, executing
@geniusgarage/ui:test:
@geniusgarage/ui:test: ✓ src/button.test.tsx (5)
@geniusgarage/ui:test: ✓ src/card.test.tsx (4)
@geniusgarage/ui:test: ✓ src/code-block.test.tsx (5)
@geniusgarage/ui:test:
@geniusgarage/ui:test: Test Files  3 passed (3)
@geniusgarage/ui:test:      Tests  14 passed (14)
@geniusgarage/ui:test:   Duration  412ms

Tasks:    1 successful, 5 total
Cached:   0 cached, 5 total
Time:     1.234s
```

**What happened:**

- Turborepo found all packages with test scripts
- Only packages/ui has tests, so only it ran
- Other packages (config, utils, web, app) have no test script, so they're skipped
- Total time: 1.234s (includes Turborepo overhead)

### 2. Run tests again (see caching)

```bash
turbo test
```

Output:

```
• Packages in scope: @geniusgarage/ui
• Running test in 1 package

@geniusgarage/ui:test: cache hit, replaying outputs

Tasks:    1 successful, 5 total
Cached:   1 cached, 5 total
Time:     127ms ⚡
```

**1.234s → 127ms!** Turborepo cached the test results and replayed them instantly.

### 3. Change a component and re-test

Edit `packages/ui/src/button.tsx`:

```tsx title="packages/ui/src/button.tsx" {2}
export function Button({ children, variant = 'primary', onClick }: ButtonProps) {
  // Add comment to invalidate cache
  const baseStyles = 'px-4 py-2 rounded-md font-semibold transition-colors'
```

Run tests:

```bash
turbo test
```

Output:

```
@geniusgarage/ui:test: cache miss, executing
@geniusgarage/ui:test: ✓ src/button.test.tsx (5)
@geniusgarage/ui:test: ✓ src/card.test.tsx (4)
@geniusgarage/ui:test: ✓ src/code-block.test.tsx (5)

Tasks:    1 successful, 5 total
Cached:   0 cached, 5 total
Time:     1.189s
```

Cache miss! Turborepo detected the source file changed and re-ran tests.

### 4. Run with --dry to see execution plan

```bash
turbo test --dry
```

Output:

```
Tasks to Run
@geniusgarage/ui:test

1 task
```

Only packages/ui has tests, so only it would run.

### 5. Force re-run with --force

Bypass cache entirely:

```bash
turbo test --force
```

Output:

```
@geniusgarage/ui:test: cache bypass, force executing
@geniusgarage/ui:test: ✓ src/button.test.tsx (5)
...

Tasks:    1 successful, 5 total
Cached:   0 cached, 5 total
Time:     1.201s
```

Tests run even though nothing changed. Useful for debugging cache issues.

## Add tests to other packages (optional)

Currently only packages/ui has tests. You can add tests to packages/utils:

Create `packages/utils/src/index.test.ts`:

```ts title="packages/utils/src/index.test.ts"
// TODO: Import formatDate, slugify, truncate, validateEmail from './index'

// TODO: Create describe block for 'formatDate'
//   - Test: 'formats date correctly'
//     - Create date: new Date('2024-01-15')
//     - Assert: formatDate(date) equals 'Jan 15, 2024'

// TODO: Create describe block for 'slugify'
//   - Test: 'converts text to slug'
//     - Assert: slugify('Hello World!') equals 'hello-world'
//   - Test: 'removes special characters'
//     - Assert: slugify('Test@#$%') equals 'test'

// TODO: Create describe block for 'truncate'
//   - Test: 'truncates long text'
//     - Assert: truncate('Hello World', 5) equals 'Hello...'
//   - Test: 'does not truncate short text'
//     - Assert: truncate('Hi', 5) equals 'Hi'

// TODO: Create describe block for 'validateEmail'
//   - Test: 'validates correct email'
//     - Assert: validateEmail('test@example.com') is true
//   - Test: 'rejects invalid email'
//     - Assert: validateEmail('invalid') is false
```

Solution

```ts title="packages/utils/src/index.test.ts"
import { describe, it, expect } from 'vitest'
import { formatDate, slugify, truncate, validateEmail } from './index'

describe('formatDate', () => {
  it('formats date correctly', () => {
    const date = new Date('2024-01-15')
    expect(formatDate(date)).toBe('Jan 15, 2024')
  })
})

describe('slugify', () => {
  it('converts text to slug', () => {
    expect(slugify('Hello World!')).toBe('hello-world')
  })

  it('removes special characters', () => {
    expect(slugify('Test@#$%')).toBe('test')
  })
})

describe('truncate', () => {
  it('truncates long text', () => {
    expect(truncate('Hello World', 5)).toBe('Hello...')
  })

  it('does not truncate short text', () => {
    expect(truncate('Hi', 5)).toBe('Hi')
  })
})

describe('validateEmail', () => {
  it('validates correct email', () => {
    expect(validateEmail('test@example.com')).toBe(true)
  })

  it('rejects invalid email', () => {
    expect(validateEmail('invalid')).toBe(false)
  })
})
```

Add vitest and test script to `packages/utils/package.json`:

```bash
pnpm add -D vitest --filter @geniusgarage/utils
```

```json title="packages/utils/package.json" {4}
{
  "scripts": {
    "lint": "eslint .",
    "test": "vitest run"
  }
}
```

Create minimal vitest config for utils (no jsdom needed for pure functions):

```ts title="packages/utils/vitest.config.ts"
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,
  },
})
```

Now run tests:

```bash
turbo test
```

Output:

```
@geniusgarage/ui:test: cache hit, replaying outputs
@geniusgarage/utils:test: cache miss, executing
@geniusgarage/utils:test: ✓ src/index.test.ts (4)

Tasks:    2 successful, 5 total
Cached:   1 cached, 5 total
Time:     891ms
```

Both packages test in parallel! packages/ui cache hit, packages/utils runs fresh.

## How test caching works

Turborepo caches test results based on:

1. **Input files** - Source files, test files, dependencies
2. **Test command** - The actual test script
3. **Environment** - Node version, env vars

**Cache invalidation happens when:**

- Source files change (button.tsx)
- Test files change (button.test.tsx)
- package.json changes (dependencies, scripts)
- Workspace dependencies change (packages/ui changes → apps using it don't re-test)

**Cache hits happen when:**

- No input changes since last run
- Same environment (Node version, etc.)
- Hash matches previous run

## Understanding test task configuration

**Your test task:**

```json
{
  "test": {
    "outputs": ["coverage/**"]
  }
}
```

**Why no dependsOn?**

```json
{
  "lint": {
    "dependsOn": ["^lint"]  // Lint dependencies first
  },
  "test": {
    // No dependsOn - tests are independent
  }
}
```

Tests don't need to wait for dependency tests to complete. packages/ui tests and apps/snippet-manager tests can run simultaneously.

**Why cache tests?**

- Tests are deterministic (same input → same output)
- Re-running unchanged tests wastes CI time
- Cached tests give instant feedback

## Commit

```bash
git add .
git commit -m "feat(turbo): add test task to pipeline"
```

## Done-when

Verify test orchestration works:

- [ ] Added test task to turbo.json
- [ ] Set outputs to \['coverage/\*\*'] for test caching
- [ ] Added test script to root package.json: `turbo test`
- [ ] Verified packages/ui has test script
- [ ] Ran `turbo test` and saw tests execute
- [ ] Ran `turbo test` again and saw cache hit
- [ ] Changed component file and saw cache miss
- [ ] Ran `turbo test --dry` and saw execution plan
- [ ] Ran `turbo test --force` to bypass cache
- [ ] Understood test tasks run independently (no dependsOn)
- [ ] Understood cache invalidates on file changes
- [ ] (Optional) Added tests to packages/utils

## What's Next

Tests are cached, but how does caching actually work? Next lesson: **Test Caching** - you'll learn exactly what triggers cache invalidation, how Turborepo hashes inputs, and strategies for maximizing cache hits in CI/CD pipelines.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
