Testing
Cloudwerk provides testing utilities built on Vitest for testing your pages, API routes, and Workers.
Install Dependencies
Section titled “Install Dependencies”pnpm add -D vitest @cloudflare/vitest-pool-workersConfiguration
Section titled “Configuration”Create vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({ test: { environment: 'miniflare', environmentOptions: { bindings: { // Test bindings }, }, },});Or use the Cloudflare pool:
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: './wrangler.toml' }, }, }, },});Testing API Routes
Section titled “Testing API Routes”Basic Route Tests
Section titled “Basic Route Tests”// app/api/users/route.test.tsimport { describe, it, expect, beforeAll, afterAll } from 'vitest';import { createTestContext } from '@cloudwerk/testing';
describe('GET /api/users', () => { let ctx: TestContext;
beforeAll(async () => { ctx = await createTestContext(); });
afterAll(async () => { await ctx.cleanup(); });
it('should return users', async () => { const response = await ctx.fetch('/api/users');
expect(response.status).toBe(200); const data = await response.json(); expect(data).toHaveProperty('users'); });
it('should require authentication', async () => { const response = await ctx.fetch('/api/users/me');
expect(response.status).toBe(401); });});Testing POST Requests
Section titled “Testing POST Requests”describe('POST /api/users', () => { it('should create a user', async () => { const response = await ctx.fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Test User', }), });
expect(response.status).toBe(201); const user = await response.json(); });
it('should validate input', async () => { const response = await ctx.fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'invalid' }), });
expect(response.status).toBe(400); const error = await response.json(); expect(error).toHaveProperty('errors'); });});Testing with Authentication
Section titled “Testing with Authentication”describe('Authenticated Routes', () => { it('should access protected route with auth', async () => { // Create authenticated context const authCtx = await ctx.withAuth({ userId: 'user-123' });
const response = await authCtx.fetch('/api/users/me');
expect(response.status).toBe(200); const user = await response.json(); expect(user.id).toBe('user-123'); });});Testing Loaders
Section titled “Testing Loaders”Page Loader Tests
Section titled “Page Loader Tests”// app/users/[id]/page.test.tsimport { describe, it, expect } from 'vitest';import { loader } from './page';import { createMockLoaderArgs } from '@cloudwerk/testing';
describe('User Page Loader', () => { it('should return user data', async () => { const args = createMockLoaderArgs({ params: { id: 'user-123' }, });
// Seed test data await args.context.db .insertInto('users') .execute();
const result = await loader(args);
expect(result.user).toMatchObject({ id: 'user-123', name: 'Test User', }); });
it('should throw NotFoundError for missing user', async () => { const args = createMockLoaderArgs({ params: { id: 'nonexistent' }, });
await expect(loader(args)).rejects.toThrow('Not found'); });});Testing Components
Section titled “Testing Components”Component Tests
Section titled “Component Tests”// app/components/UserCard.test.tsximport { describe, it, expect } from 'vitest';import { render, screen } from '@testing-library/react';import { UserCard } from './UserCard';
describe('UserCard', () => { it('should render user name', () => {
expect(screen.getByText('John Doe')).toBeInTheDocument(); });
it('should handle missing avatar', () => {
expect(screen.getByText('J')).toBeInTheDocument(); // Initials fallback });});Testing Middleware
Section titled “Testing Middleware”// app/middleware.test.tsimport { describe, it, expect } from 'vitest';import { middleware } from './middleware';import { createMockRequest, createMockContext } from '@cloudwerk/testing';
describe('Auth Middleware', () => { it('should allow authenticated requests', async () => { const request = createMockRequest('/api/protected', { headers: { Cookie: 'session=valid-session' }, }); const context = createMockContext({ auth: { getUser: async () => ({ id: 'user-123' }) }, }); const next = vi.fn().mockResolvedValue(new Response('OK'));
const response = await middleware(request, next, context);
expect(next).toHaveBeenCalled(); expect(response.status).toBe(200); });
it('should reject unauthenticated requests', async () => { const request = createMockRequest('/api/protected'); const context = createMockContext({ auth: { getUser: async () => null }, }); const next = vi.fn();
const response = await middleware(request, next, context);
expect(next).not.toHaveBeenCalled(); expect(response.status).toBe(401); });});Testing Queue Handlers
Section titled “Testing Queue Handlers”// workers/email-queue.test.tsimport { describe, it, expect, vi } from 'vitest';import handler from './email-queue';import { createMockMessage, createMockEnv } from '@cloudwerk/testing';
describe('Email Queue Handler', () => { it('should process email messages', async () => { const env = createMockEnv(); const message = createMockMessage({ });
await handler.queue([message], env, {});
expect(message.ack).toHaveBeenCalled(); expect(env.EMAIL_API.send).toHaveBeenCalledWith({ template: 'welcome', }); });
it('should retry on failure', async () => { const env = createMockEnv(); env.EMAIL_API.send.mockRejectedValue(new Error('API Error'));
const message = createMockMessage({ });
await handler.queue([message], env, {});
expect(message.retry).toHaveBeenCalled(); expect(message.ack).not.toHaveBeenCalled(); });});Testing Durable Objects
Section titled “Testing Durable Objects”// workers/counter.test.tsimport { describe, it, expect } from 'vitest';import { Counter } from './counter';import { createMockDurableObjectState } from '@cloudwerk/testing';
describe('Counter Durable Object', () => { it('should increment count', async () => { const state = createMockDurableObjectState(); const counter = new Counter(state, {});
const response = await counter.fetch( new Request('http://counter/increment') );
expect(await response.text()).toBe('1'); });
it('should persist count', async () => { const state = createMockDurableObjectState(); const counter = new Counter(state, {});
await counter.fetch(new Request('http://counter/increment')); await counter.fetch(new Request('http://counter/increment')); await counter.fetch(new Request('http://counter/increment'));
const response = await counter.fetch(new Request('http://counter/'));
expect(await response.text()).toBe('3'); expect(state.storage.put).toHaveBeenCalledWith('count', 3); });});Testing Utilities
Section titled “Testing Utilities”createTestContext()
Section titled “createTestContext()”Create a full test context:
const ctx = await createTestContext({ // Seed database seed: async (db) => { await db.insertInto('users').values([ { id: '1', name: 'User 1' }, { id: '2', name: 'User 2' }, ]).execute(); },});createMockLoaderArgs()
Section titled “createMockLoaderArgs()”Create mock loader arguments:
const args = createMockLoaderArgs({ params: { id: '123' }, request: new Request('http://localhost/users/123'), context: { env: { API_KEY: 'test-key' }, },});createMockRequest()
Section titled “createMockRequest()”Create mock requests:
const request = createMockRequest('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Test' }),});createMockContext()
Section titled “createMockContext()”Create mock context:
const context = createMockContext({ env: { DB: mockDb, KV: mockKv }, auth: { getUser: async () => mockUser },});Running Tests
Section titled “Running Tests”# Run all testspnpm test
# Watch modepnpm test --watch
# Coveragepnpm test --coverage
# Specific filepnpm test app/api/users/route.test.tsNext Steps
Section titled “Next Steps”- CLI Reference - Test command options
- Configuration - Test configuration
- Examples - See tests in action