#
Testing
Latest
Backend testing comprises three distinct test types that, combined with code coverage metrics values near one hundred percent, collectively deliver comprehensive coverage across all application layers.
#
Dependencies
Given that the backend operates exclusively on the workerd runtime, the test runner must utilize this runtime as well. Cloudflare provides the @cloudflare/vitest-pool-workers package, which provides an isolated runtime instance for each test run. Consequently, Vitest serves as the designated test runner.
Limitation
Although workerd is built on top of the V8 engine, code coverage cannot be generated through the @vitest/coverage-v8 package. The @vitest/coverage-istanbul package should be used instead. This limitation is considered a known issue.
A complete test configuration therefore comprises the following packages:
{
"devDependencies": {
"@cloudflare/vitest-pool-workers": "latest",
"@vitest/coverage-istanbul": "latest",
"vitest": "latest",
}
}
Info
Ensure the latest compatible Vitest version within the official prerequisites before upgrading. New minor versions of Vitest typically require several weeks before becoming guatanteed to work with the workerd runtime.
#
Unit Test
Validates functionality of individual functions stored within files located in the src/utils directory. Tests should be developed within a context of limited knowledge regarding the function's further usage, ensuring test cases address all reasonable scenarios.
#
Structure
Unit tests should be created inside the test/unit directory, maintaining a file-for-file approach rather than a file-for-function approach, effectively mirroring the src/utils directory.
src/
├─ utils/
│ ├─ crypto.ts
│ ├─ bank.ts
│ ├─ database/
│ │ ├─ user.ts
│ │ ├─ party.ts
│ │ ├─ organization.ts
test/
├─ unit/
│ ├─ crypto.test.ts
│ ├─ bank.test.ts
│ ├─ database/
│ │ ├─ user.test.ts
│ │ ├─ party.test.ts
│ │ ├─ organization.test.ts
// Name of the function being tested
describe('nameOfTheFunction', () => {
// Expected positive result - list positive scenarios first
describe('expected positive result', () => {
// When positive result should occur
it('when', () => {
// ...
})
})
// Expected negative result
describe('expected negative result', () => {
// When negative result should occur
it('when', () => {
// ...
})
})
})
#
Integration Test
Validates functionality of individual actions stored within the src/index.ts file of each service. Tests should be developed within a context of full knowledge regarding the actions's further usage, as actions are created to address specific business needs. Test cases should encompass the positive path and only business-logic-possible negative paths.
#
Structure
Integration tests should be created inside the test/integration directory, maintaining a file-for-action approach, effectively extracting actions from the src/index.ts file.
src/
├─ index.ts
test/
├─ integration/
│ ├─ createUser.test.ts
│ ├─ deleteUser.test.ts
│ ├─ getOrganization.test.ts
│ ├─ updateUser.test.ts
// Name of the action being tested
describe('nameOfTheAction', () => {
// Expected positive result - list positive scenarios first
describe('expected positive result', () => {
// When positive result should occur
it('when', () => {
// ...
})
})
// Expected negative result
describe('expected negative result', () => {
// When negative result should occur
it('when', () => {
// ...
})
})
})
#
Actions
All actions of the service are accessible through SELF fetcher provided by cloudflare:test import. This fetcher is a binding to the default export of the service. If an action accesses any bindings within its execution, the entire environment is created locally by Miniflare before the test run and should not present testing obstacles.
#
Storage
When testing an action that manipulates data within any storage, such operations should be confirmed after the action call explicitly by querying the storage for the manipulated data, expecting the data to be returned in the correct form.
describe('createOrganizationUser', () => {
const db = drizzle(env.ORG_DB, { schema: tables })
const organizationId = uuidv4()
beforeEach(async () => {
await setupOrganization(db, { id: organizationId })
})
describe('should create a new user when', () => {
it('the organization exists', async () => {
const { status, message, data, error } =
await SELF.createOrganizationUser({
organizationId,
creatorRole: 'ADMIN',
email: 'talon@runeterra.com',
createdRole: 'MEMBER',
})
expect(error).toBeFalsy()
expect(status).toEqual(200)
expect(message).toEqual('Organization user successfully created.')
expect(data).toEqual({
id: data?.id,
createdAt: data?.createdAt,
modifiedAt: data?.modifiedAt,
email: 'talon@runeterra.com',
password: '[REDACTED]',
salt: '[REDACTED]',
role: 'MEMBER',
state: 'UNVERIFIED',
organizationId,
})
const dbCheck = await db
.select()
.from(tables.user)
.where(
and(
eq(user.organizationId, organizationId),
eq(user.email, 'talon@runeterra.com'),
),
)
.execute()
expect(dbCheck).toEqual([
{
createdAt: dbCheck[0].createdAt,
email: 'talon@runeterra.com',
id: dbCheck[0].id,
modifiedAt: dbCheck[0].modifiedAt,
organizationId: organizationId,
password: dbCheck[0].password,
role: 'MEMBER',
salt: dbCheck[0].salt,
state: 'UNVERIFIED',
},
])
})
})
}
#
E2E Test
Validates functionality of individual endpoints stored within the server/api directory of the orchestrator. Tests should be developed within a context of full knowledge regarding the endpoint's further usage, as endpoints are created to address specific business need. Test cases should avoid duplicate coverage already provided by integration tests while focusing on Zod schema validation and endpoint-level business logic.
Info
All applications must be running when E2E tests are executed. No mocking occurs as these tests should simulate actual behavior.
#
Structure
E2E tests should be created inside the test/e2e directory. The structure should maintain a file-for-endpoint approach, effectively mirroring the server/api directory without subdirectories.
server/
├─ api/
│ ├─ v1/
│ │ ├─ users/
│ │ │ ├─ [id]/
│ │ │ │ ├─ index.delete.ts
│ │ │ │ ├─ index.patch.ts
│ │ │ ├─ index.post.ts
│ │ ├─ index.get.ts
test/
├─ e2e/
│ ├─ users/
│ │ ├─ createUser.test.ts
│ │ ├─ deleteUser.test.ts
│ │ ├─ updateUser.test.ts
│ ├─ healthcheck.test.ts
// Name of the endpoint being tested
describe('nameOfTheEndpoint', () => {
// Expected positive result - list positive scenarios first
describe('expected positive result', () => {
// When positive result should occur
it('when', () => {
// ...
})
})
// Expected negative result
describe('expected negative result', () => {
// When negative result should occur
it('when', () => {
// ...
})
})
})
#
Configuration
The Vitest configuration operates at two levels with a root-level vitest.config.ts file for high-level settings and per-application vitest.config.ts file containing application-specific configuration.
#
Root
The root configuration file contains settings defining the projects within the monorepository alongside the coverage configuration. The standard Vitest function defineConfig is utilized.
import { coverageConfigDefaults, defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// Define paths to all applications within the repository
projects: [
'apps/**/vitest.config.ts',
'packages/**/vitest.config.ts',
'services/**/vitest.config.ts',
],
coverage: {
// Set Instanbul as the provider of code coverage
provider: 'istanbul',
// Explicitly add all config files to the list of coverage excluded files
exclude: [...coverageConfigDefaults.exclude, '**/*.config.ts'],
},
},
})
#
Application
The application-specific file contains settings related exclusively to the application as the Wrangler configuration. The defineWorkersProject function from the @cloudflare/vitest-pool-workers package is utilized.
import { defineWorkersProject } from '@cloudflare/vitest-pool-workers/config'
export default defineWorkersProject({
test: {
poolOptions: {
workers: {
wrangler: {
// Define a path to the Wrangler configuration
configPath: './wrangler.jsonc',
},
},
},
},
})
#
Scripts
For enhanced developer experience, use commands to execute tests. Each test type must be executable independently through specific command. Code coverage must be available per-application as well. Use the test:[where]:[what] format when composing commands.
{
"scripts": {
"test:all": "vitest",
"test:coverage": "vitest --coverage",
"test:auth": "vitest --project @services/auth",
"test:auth:cov": "vitest run --project @services/auth --coverage --coverage.include=services/auth/src",
"test:auth:unit": "vitest --project @services/auth unit fixtures",
"test:auth:int": "vitest --project @services/auth integration",
"test:auth:e2e": "vitest --project @apps/gateway e2e/auth",
}
}
#
TypeScript
To leverage features such as SELF fetcher or bindings, TypeScript configuration must be implemented on a per-service basis.
-
- Add Types
Add
@cloudflare/vitest-pool-workersto thetypeskey of the roottsconfig.json.
{
"types": [
"@cloudflare/vitest-pool-workers",
"@cloudflare/workers-types",
"@services/auth/worker-configuration.d.ts",
"node"
]
}
-
- Create Separate Configuration
Create a file named
tsconfig.jsonwithin thetestdirectory.
{
"extends": "../../tsconfig.json"
}
-
- Provide Environment Types
Create a file named
env.d.tswithin thetestdirectory.
declare module 'cloudflare:test' {
// TODO: Change the name of the `Env` interface to the one of the service
interface ProvidedEnv extends Env {}
export const SELF: Service<import('../src/index').default>
}
#
Service Bindings
Service bindings are the only bindings not automatically generated locally by Miniflare prior to test execution. However, built services can be provided to Miniflare through external configuration.
-
- Build Services
Create a file named
build-services.tswithin thetest/setupdirectory.
import { spawn } from 'node:child_process'
const buildWorker = async (cwd: string) => {
const child = spawn('bun', ['wrangler', 'build'], { cwd })
child.stdout?.on('data', (data) => process.stdout.write(data))
child.stderr?.on('data', (data) => process.stderr.write(data))
return new Promise<number>((resolve) => {
child.on('close', (code) => resolve(code ?? -1))
})
}
export default async () => {
await buildWorker('./services/cryptography')
await buildWorker('./services/ledger')
}
-
- Register Setup File
Add the
globalSetupkey with the setup file path value to thetestkey within the application-level Vitest configuration file.
import { defineWorkersProject } from '@cloudflare/vitest-pool-workers/config'
export default defineWorkersProject({
test: {
globalSetup: ['./test/setup/build-services.ts'],
poolOptions: {
workers: {
wrangler: {
configPath: './wrangler.jsonc',
},
},
},
},
})
-
- Provide to Miniflare
Add the
miniflarekey with the configuration of the services to thetest.poolOptions.workerskey within the application-level Vitest configuration file.
Limitation
The configuration of bound services cannot be retrieved from the corresponding wrangler.jsonc file, therefore the configuration must be duplicated directly within the configuration file.
import { defineWorkersProject } from '@cloudflare/vitest-pool-workers/config'
export default defineWorkersProject({
test: {
globalSetup: ['./test/setup/build-services.ts'],
poolOptions: {
workers: {
miniflare: {
workers: [
{
name: 'cryptography-service',
modules: [
{
path: '../cryptography/dist/index.js',
type: 'ESModule',
},
],
compatibilityDate: '2024-12-30',
compatibilityFlags: ['nodejs_compat'],
},
{
name: 'ledger-service',
modules: [
{
path: '../ledger/dist/index.js',
type: 'ESModule',
},
],
compatibilityDate: '2024-12-30',
},
],
},
wrangler: {
configPath: './wrangler.jsonc',
},
},
},
},
})