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 @vitest/coverage-istanbul @cloudflare/vitest-pool-workers 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'],
            },
          ],
        },
      }
    )]
  }
)

Test Setup

All data that needs to be set up should be placed at the beginning of the test file in the beforeEach() Vitest function. If any data needs to be set up later, make sure to clean it up in the same test case.

Unit Tests

Database Utils (Queries and Commands)

Query

Check the whole object returned, ideally without base, and add a negative test case when the requested data was not found.

Command

  • CREATE: Check the whole object returned, ideally without base.
  • UPDATE: Check only the updated field and one field to verify the correct object was updated (e.g. id).
  • DELETE: Check if deletedAt is set to a new date and one field to verify the correct object was updated (e.g. id).

Test structure:

ts
describe('Function being tested', () => {
  describe('should succed when', () => {
    it('xyz', () => {
      expect(true).toBeTruthy()
    })
  })
  
  describe('should throw when', () => {
    it('abc', () => {
      expect(true).toBeTruthy()
    })
  })
})

Utils (Non-Database)

For everything else in the utils folder (except database utils), test all possible scenarios — both negative and positive.

Integration Tests

Integration tests cover all actions, queues, and crons.

  • Should verify that utils work correctly together.
  • When it comes to output, only check crucial values, not whole objects — full object comparisons are already covered by unit tests.
  • Do not repeat test cases from unit tests.
  • If a given action connects to outside services, check if the connection was completed successfully (with a service mock).
  • Should verify that errors throw as expected, checking the fields: error, message, and status.
  • Focus on the "user path".
  • Avoid testing implementation details. If testing a function requires changing it from private to public, skip the test.

E2E Tests

E2E tests cover the endpoints in the form of flows that the user has to go through while using the application.