Testing
Alchemy provides built-in testing utilities powered by Vitest for testing infrastructure resources. Tests create real resources, verify their behavior, and clean up automatically.
Setting Up Tests
Alchemy tests use alchemy.test() to create isolated test scopes:
import { describe, expect } from "vitest";
import { destroy } from "alchemy/test";
import "alchemy/test/vitest";
import { Worker } from "alchemy/cloudflare";
const test = alchemy.test(import.meta, {
prefix: "test"
});
describe("Worker", () => {
test("creates and deletes a worker", async (scope) => {
let worker;
try {
// Create resource
worker = await Worker("test-worker", {
script: "export default { fetch() { return new Response('Hello'); } }"
});
// Verify it was created
expect(worker.id).toBe("test-worker");
expect(worker.url).toContain("workers.dev");
// Test functionality
const response = await fetch(worker.url);
expect(response.ok).toBe(true);
expect(await response.text()).toBe("Hello");
} finally {
// Always clean up
await destroy(scope);
}
});
});
Test Structure
Test Lifecycle
Alchemy tests follow a consistent pattern:
Every test gets an isolated scope:
const test = alchemy.test(import.meta, {
prefix: "test" // Prefix for all test resources
});
Execute: Create and Test Resources
Create resources and verify their behavior:
test("resource creation", async (scope) => {
const resource = await MyResource("test-id", {
// props
});
expect(resource).toMatchObject({
// assertions
});
});
Cleanup: Destroy Resources
Always clean up in a finally block:
try {
// Create and test resources
} finally {
await destroy(scope);
}
Test Configuration
Test Prefix
Use a unique prefix to avoid conflicts between test runs:
import { BRANCH_PREFIX } from "./util";
const test = alchemy.test(import.meta, {
prefix: BRANCH_PREFIX // e.g., "feat-auth-123"
});
describe("Resources", () => {
test("creates worker", async (scope) => {
const worker = await Worker("api", { /* ... */ });
// Resource name: "feat-auth-123-test-file-name-api"
});
});
Use branch names or CI job IDs as prefixes to ensure tests from different branches don’t conflict.
Test Timeout
Set timeout for long-running tests:
test("deploys large application", async (scope) => {
// Test logic
}, 300000); // 5 minute timeout
Quiet Mode
Disable logging in tests:
const test = alchemy.test(import.meta, {
prefix: "test",
quiet: true // Suppress resource creation logs
});
Testing Patterns
Create, Update, Delete
Test the full resource lifecycle:
test("worker lifecycle", async (scope) => {
let worker;
try {
// CREATE
worker = await Worker("api", {
script: "export default { fetch() { return new Response('v1'); } }"
});
expect(worker.id).toBe("api");
let response = await fetch(worker.url);
expect(await response.text()).toBe("v1");
// UPDATE
worker = await Worker("api", {
script: "export default { fetch() { return new Response('v2'); } }"
});
response = await fetch(worker.url);
expect(await response.text()).toBe("v2");
} finally {
// DELETE
await destroy(scope);
// Verify deletion
await expect(fetch(worker.url)).rejects.toThrow();
}
});
Resource Dependencies
Test resources that depend on each other:
test("worker with database", async (scope) => {
let db, worker;
try {
// Create database first
db = await D1Database("db", {
name: "test-database"
});
// Create worker that uses database
worker = await Worker("api", {
entrypoint: "./src/index.ts",
bindings: {
DB: db
}
});
// Test that binding works
const response = await fetch(worker.url + "/users");
expect(response.ok).toBe(true);
} finally {
await destroy(scope);
}
});
End-to-End Testing
Test complete workflows:
test("complete application stack", async (scope) => {
try {
// 1. Infrastructure
const db = await D1Database("db", { name: "test-db" });
const bucket = await R2Bucket("storage", { name: "test-bucket" });
const kv = await KVNamespace("cache", { name: "test-cache" });
// 2. API Worker
const api = await Worker("api", {
entrypoint: "./src/api.ts",
bindings: { DB: db, BUCKET: bucket, CACHE: kv }
});
// 3. Frontend
const frontend = await Vite("frontend", {
bindings: { API: api }
});
// 4. Test end-to-end flow
const response = await fetch(frontend.url + "/api/health");
expect(response.ok).toBe(true);
} finally {
await destroy(scope);
}
});
Error Handling
Test error conditions:
test("handles invalid configuration", async (scope) => {
await expect(async () => {
await Worker("invalid", {
// Missing required entrypoint
});
}).rejects.toThrow("entrypoint is required");
});
test("handles resource conflicts", async (scope) => {
try {
const worker1 = await Worker("api", {
name: "my-worker",
script: "export default {}"
});
// Should fail without adopt: true
await expect(async () => {
await Worker("api-2", {
name: "my-worker", // Same name
script: "export default {}"
});
}).rejects.toThrow("already exists");
} finally {
await destroy(scope);
}
});
Verification Helpers
Fetch and Expect OK
Create a helper for testing HTTP endpoints:
async function fetchAndExpectOK(url: string) {
const response = await fetch(url);
expect(response.ok).toBe(true);
return response;
}
test("worker responds successfully", async (scope) => {
try {
const worker = await Worker("api", {
script: "export default { fetch() { return new Response('OK'); } }"
});
await fetchAndExpectOK(worker.url);
} finally {
await destroy(scope);
}
});
Resource Existence Verification
Verify resources are truly deleted:
async function assertWorkerDoesNotExist(api: CloudflareApi, name: string) {
const response = await api.get(`/workers/scripts/${name}`);
expect(response.status).toBe(404);
}
test("worker deletion", async (scope) => {
const api = createCloudflareApi();
let worker;
try {
worker = await Worker("api", {
script: "export default {}"
});
} finally {
await destroy(scope);
await assertWorkerDoesNotExist(api, worker.name);
}
});
Test Hooks
beforeAll and afterAll
Run setup and teardown for all tests:
const test = alchemy.test(import.meta, {
prefix: "test"
});
let sharedResource;
test.beforeAll(async (scope) => {
// Create resources shared across all tests
sharedResource = await D1Database("shared-db", {
name: "shared-database"
});
});
test.afterAll(async (scope) => {
// Clean up shared resources
await destroy(scope);
});
describe("API Tests", () => {
test("test 1", async (scope) => {
// Use sharedResource
});
test("test 2", async (scope) => {
// Use sharedResource
});
});
Running Tests
Run All Tests
This automatically:
- Diffs with
main branch
- Runs only changed tests
- Requires you to be on a feature branch
Run Specific Tests
# Run specific file
bun vitest ./test/cloudflare/worker.test.ts
# Run specific test case
bun vitest ./test/cloudflare/worker.test.ts -t "creates worker"
# Run all tests in a directory
bun vitest ./test/cloudflare/
Watch Mode
Re-run tests on file changes:
Test Environment Variables
Tests require provider credentials:
# .env.test
CLOUDFLARE_ACCOUNT_ID=your-account-id
CLOUDFLARE_API_KEY=your-api-key
CLOUDFLARE_EMAIL=your-email
ALCHEMY_PASSWORD=test-password
Load them in your test file:
import "dotenv/config";
import { describe, expect } from "vitest";
import "alchemy/test/vitest";
const test = alchemy.test(import.meta, {
prefix: "test",
password: process.env.ALCHEMY_PASSWORD
});
State Storage for Tests
Tests use SQLite by default for state storage:
const test = alchemy.test(import.meta, {
prefix: "test",
stateStore: (scope) => new SQLiteStateStore(scope, {
filename: path.join(scope.dotAlchemy, "test.sqlite")
})
});
Or use other state stores:
import { D1StateStore, CloudflareStateStore } from "alchemy/state";
// D1 Database
const test = alchemy.test(import.meta, {
stateStore: (scope) => new D1StateStore(scope)
});
// Cloudflare Durable Objects
const test = alchemy.test(import.meta, {
stateStore: (scope) => new CloudflareStateStore(scope)
});
Best Practices
Use Deterministic Resource IDs
Avoid random IDs to make tests idempotent:
// ✅ Good - Deterministic
const worker = await Worker("test-api", { /* ... */ });
// ❌ Bad - Random IDs
const worker = await Worker(`test-${Math.random()}`, { /* ... */ });
Ensure cleanup even when tests fail:
test("resource test", async (scope) => {
let resource;
try {
resource = await MyResource("test", { /* ... */ });
// Test logic that might throw
} finally {
await destroy(scope);
}
});
Test Create, Update, and Delete
Cover the full lifecycle:
test("full lifecycle", async (scope) => {
try {
// Create
let worker = await Worker("api", { script: "v1" });
expect(worker).toBeDefined();
// Update
worker = await Worker("api", { script: "v2" });
expect(worker).toBeDefined();
} finally {
// Delete
await destroy(scope);
}
});
import { BRANCH_PREFIX } from "./util";
const test = alchemy.test(import.meta, {
prefix: `${BRANCH_PREFIX}-${Date.now()}`
});
Confirm resources are truly gone:
finally {
await destroy(scope);
// Verify deletion
const response = await api.get(`/resources/${resource.id}`);
expect(response.status).toBe(404);
}
Avoid dynamic imports for better IDE support:
// ✅ Good
import { Worker } from "alchemy/cloudflare";
// ❌ Avoid
const { Worker } = await import("alchemy/cloudflare");
Example: Complete Test Suite
import { describe, expect } from "vitest";
import { destroy } from "alchemy/test";
import "alchemy/test/vitest";
import { Worker, D1Database, R2Bucket } from "alchemy/cloudflare";
import { BRANCH_PREFIX } from "../util";
const test = alchemy.test(import.meta, {
prefix: BRANCH_PREFIX,
quiet: true
});
describe("Application Stack", () => {
test("creates full application", async (scope) => {
let db, bucket, worker;
try {
// Create infrastructure
db = await D1Database("db", {
name: `${BRANCH_PREFIX}-database`
});
expect(db.id).toBe("db");
expect(db.name).toContain(BRANCH_PREFIX);
bucket = await R2Bucket("storage", {
name: `${BRANCH_PREFIX}-bucket`
});
expect(bucket.id).toBe("storage");
// Create worker with bindings
worker = await Worker("api", {
script: `
export default {
async fetch(request, env) {
return new Response('OK');
}
}
`,
bindings: {
DB: db,
BUCKET: bucket
}
});
expect(worker.id).toBe("api");
expect(worker.url).toBeTruthy();
// Test worker responds
const response = await fetch(worker.url);
expect(response.ok).toBe(true);
expect(await response.text()).toBe("OK");
} finally {
await destroy(scope);
}
}, 120000); // 2 minute timeout
test("handles updates", async (scope) => {
let worker;
try {
// Create v1
worker = await Worker("api", {
script: "export default { fetch() { return new Response('v1'); } }"
});
let response = await fetch(worker.url);
expect(await response.text()).toBe("v1");
// Update to v2
worker = await Worker("api", {
script: "export default { fetch() { return new Response('v2'); } }"
});
response = await fetch(worker.url);
expect(await response.text()).toBe("v2");
} finally {
await destroy(scope);
}
});
});
Troubleshooting
Test Timeouts
Increase timeout for slow resources:
test("slow deployment", async (scope) => {
// Test logic
}, 300000); // 5 minutes
State Conflicts
Use unique prefixes to avoid conflicts:
const test = alchemy.test(import.meta, {
prefix: `${process.env.CI_JOB_ID}-test`
});
Cleanup Failures
Manually destroy orphaned resources:
bun ./alchemy.run.ts --destroy --stage test-stage
Next Steps