Skip to content

Testing

Vitest is used for unit and integration testing. Since all applications run within a workerd environment, the cloudflareTest plugin from @cloudflare/vitest-pool-workers must be used.

This page doesn't cover E2E testing which is handled by Playwright and Cloudflare Browser Rendering. Find out more information on this page about E2E testing.

Coverage

Native code coverage via V8 is not supported. Instrumented code coverage must be used instead, so the @vitest/coverage-istanbul package is required.

Configuration

Use following command to install all needed dependencies:

zsh
bun add -D vitest@^4.1.2 @vitest/coverage-istanbul@^4.1.2 @cloudflare/vitest-pool-workers@^0.13.5 vite-tsconfig-paths

Since all projects utilize a monorepo architecture, using Vitest projects is crucial for performance and predictability.

A basic root vitest.config.ts file:

ts
import { coverageConfigDefaults, defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    printConsoleTrace: true,
    globalSetup: ['./scripts/test/build-workers.ts'],
    projects: [
      'apps/**/vitest.config.ts',
      'packages/**/vitest.config.ts',
      'services/**/vitest.config.ts',
    ],
    coverage: {
      provider: 'istanbul',
      include: [
        'apps/*/src/**/*.ts',
        'services/*/src/**/*.ts',
        'packages/*/src/**/*.ts',
      ],
      exclude: [
        ...coverageConfigDefaults.exclude,
        '**/*.config.ts'
      ],
    }
  }
})

A test scripts for a root:

ts
{
  "scripts": {
    "test": "vitest",
    "test:cov": "vitest run --coverage",
    "test:unit": "vitest unit fixtures",
    "test:int": "vitest integration" 
  }
}

A basic project vitest.config.ts for any worker:

ts
import { cloudflareTest } from '@cloudflare/vitest-pool-workers'
import tsconfigPaths from 'vite-tsconfig-paths'
import { defineConfig } from 'vitest/config'

export default defineConfig({
  plugins: [
    tsconfigPaths({
      projects: ['../../tsconfig.base.json'],
    }),
    cloudflareTest({
      wrangler: {
        configPath: './wrangler.jsonc',
      },
    }),
  ],
})

A test script for any worker:

json
{
  "scripts": {
    "test": "vitest --project $npm_package_name --root ../.."
  }
}

Bindings

Miniflare does a great job simulating most bindings, requiring no additional test context to set them up. This section covers the ones that are not straightforward to configure.

Services

Some workers may have bound services that are simply other workers. However, before running tests on a worker with bound services, those services must be built first. This is handled automatically by executing the scripts/test/build-workers.ts script before Vitest runs.

ts
import { spawn } from 'node:child_process'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'

const monorepoRoot = path.resolve(
  path.dirname(fileURLToPath(import.meta.url)),
  '../..',
)

const buildWorker = async (relativePath: string) => {
  const cwd = path.resolve(monorepoRoot, relativePath)
  const child = spawn('bunx', ['wrangler', 'build'], { cwd })

  child.stdout?.on('data', (data: string) => process.stdout.write(data))
  child.stderr?.on('data', (data: string) => process.stderr.write(data))

  return new Promise<number>((resolve) => {
    child.on('close', (code: number) => resolve(code ?? -1))
  })
}

export default async () => {
  // TODO: Add all project workers
  // await buildWorker('apps/gateway')
  // await buildWorker('apps/secrets-store')
  // await buildWorker('apps/...')
  // await buildWorker('services/bank')
  // await buildWorker('services/notification')
  // await buildWorker('services/...')
}

Even though the services are defined in services key of wrangler.jsonc, they need to be explicitely defined for Miniflare in vitest.config.ts. Make sure the name of each service is exact match as in it's wrangler.jsonc.

An example of project vitest.config.ts for any worker extended of service bindings:

ts
// Other imports...
import { resolve } from '@std/path'

const repositoryRoot = resolve(import.meta.dirname, '../..')

export default defineConfig({
    // Other keys...
    plugins: [
      // Other plugins...
      cloudflareTest({
        miniflare: {
          workers: [
            {
              name: 'project-name-secrets-store',
              modulesRoot: repositoryRoot,
              modules: [
                {
                  type: 'ESModule',
                  path: resolve(repositoryRoot, 'apps/secrets-store/dist/index.js'),
                },
              ],
              compatibilityDate: '2025-06-04',
              compatibilityFlags: ['nodejs_compat'],
            },
            {
              name: 'project-name-bank-service',
              modulesRoot: repositoryRoot,
              modules: [
                {
                  type: 'ESModule',
                  path: resolve(repositoryRoot, 'services/bank/dist/index.js'),
                },
              ],
              compatibilityDate: '2025-06-04',
              compatibilityFlags: ['nodejs_compat'],
            },
            {
              name: 'project-name-notification-service',
              modulesRoot: repositoryRoot,
              modules: [
                {
                  type: 'ESModule',
                  path: resolve(repositoryRoot, 'services/notification/dist/index.js'),
                },
              ],
              compatibilityDate: '2025-06-04',
              compatibilityFlags: ['nodejs_compat'],
            },
          ],
        },
      }
    )]
  }
)