Skip to main content

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:
1
Setup: Create Test Scope
2
Every test gets an isolated scope:
3
const test = alchemy.test(import.meta, {
  prefix: "test"  // Prefix for all test resources
});
4
Execute: Create and Test Resources
5
Create resources and verify their behavior:
6
test("resource creation", async (scope) => {
  const resource = await MyResource("test-id", {
    // props
  });
  
  expect(resource).toMatchObject({
    // assertions
  });
});
7
Cleanup: Destroy Resources
8
Always clean up in a finally block:
9
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

bun run test
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:
bun vitest --watch

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

1
Use Deterministic Resource IDs
2
Avoid random IDs to make tests idempotent:
3
// ✅ Good - Deterministic
const worker = await Worker("test-api", { /* ... */ });

// ❌ Bad - Random IDs
const worker = await Worker(`test-${Math.random()}`, { /* ... */ });
4
Always Use try/finally
5
Ensure cleanup even when tests fail:
6
test("resource test", async (scope) => {
  let resource;
  
  try {
    resource = await MyResource("test", { /* ... */ });
    // Test logic that might throw
  } finally {
    await destroy(scope);
  }
});
7
Test Create, Update, and Delete
8
Cover the full lifecycle:
9
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);
  }
});
10
Use Unique Prefixes
11
Prevent test conflicts:
12
import { BRANCH_PREFIX } from "./util";

const test = alchemy.test(import.meta, {
  prefix: `${BRANCH_PREFIX}-${Date.now()}`
});
13
Verify Resource Deletion
14
Confirm resources are truly gone:
15
finally {
  await destroy(scope);
  
  // Verify deletion
  const response = await api.get(`/resources/${resource.id}`);
  expect(response.status).toBe(404);
}
16
Use Static Imports
17
Avoid dynamic imports for better IDE support:
18
// ✅ 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

Build docs developers (and LLMs) love