# 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.

A complete test configuration therefore comprises the following packages:

package.json
{
  "devDependencies": {
    "@cloudflare/vitest-pool-workers": "latest",
    "@vitest/coverage-istanbul": "latest",
    "vitest": "latest",
  }
}

# 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.

Implementation
Tests
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
Unit Test Pattern
// 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.

Implementation
Tests
src/
├─ index.ts
test/
├─ integration/
│  ├─ createUser.test.ts
│  ├─ deleteUser.test.ts
│  ├─ getOrganization.test.ts
│  ├─ updateUser.test.ts
Integration Test Pattern
// 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.

test/integration/createOrganizationUser.test.ts
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.

# 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.

Implementation
Tests
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
E2E Test Pattern
// 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.

vitest.config.ts
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.

service/auth/vitest.config.ts
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.

package.json
{
  "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.

  1. Add Types

    Add @cloudflare/vitest-pool-workers to the types key of the root tsconfig.json.

tsconfig.json
{
  "types": [
    "@cloudflare/vitest-pool-workers",
    "@cloudflare/workers-types",
    "@services/auth/worker-configuration.d.ts",
    "node"
  ]
}
  1. Create Separate Configuration

    Create a file named tsconfig.json within the test directory.

service/auth/test/tsconfig.json
{
  "extends": "../../tsconfig.json"
}
  1. Provide Environment Types

    Create a file named env.d.ts within the test directory.

services/auth/test/env.d.ts
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.

  1. Build Services

    Create a file named build-services.ts within the test/setup directory.

test/setup/build-services.ts
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')
}
  1. Register Setup File

    Add the globalSetup key with the setup file path value to the test key within the application-level Vitest configuration file.

services/auth/vitest.config.ts
import { defineWorkersProject } from '@cloudflare/vitest-pool-workers/config'

export default defineWorkersProject({
  test: {
    globalSetup: ['./test/setup/build-services.ts'],
    poolOptions: {
      workers: {
        wrangler: {
          configPath: './wrangler.jsonc',
        },
      },
    },
  },
})
  1. Provide to Miniflare

    Add the miniflare key with the configuration of the services to the test.poolOptions.workers key within the application-level Vitest configuration file.

services/auth/vitest.config.ts
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',
        },
      },
    },
  },
})