Skip to content

Testing

Alchemy provides test utilities that integrate with Bun and Vitest, wrapping their test APIs with Effect support and stack lifecycle management.

The test harness provides Effect-aware versions of test, beforeAll, afterAll, and expect:

import { beforeAll, deploy, expect, test } from "alchemy/Test/Bun";

Each test(name, effect) runs an Effect generator instead of an async function. The harness provides platform layers (HttpClient, etc.) automatically.

deploy(Stack) returns an Effect that plans and applies a stack, resolving to its outputs. destroy(Stack) tears it down:

import { afterAll, beforeAll, deploy, destroy } from "alchemy/Test/Bun";
import Stack from "../alchemy.run.ts";
const stack = beforeAll(deploy(Stack));
afterAll.skipIf(!process.env.CI)(destroy(Stack));
  • beforeAll(effect) runs the Effect once before all tests and returns a lazy accessor
  • afterAll.skipIf(!process.env.CI) skips destroy locally for fast iteration
  • On CI (CI=true), the stack is torn down after tests complete

Use yield* stack inside a test to get the deployed stack’s outputs:

test(
"worker is reachable",
Effect.gen(function* () {
const { url } = yield* stack;
expect(url).toBeString();
}),
);

HttpClient is provided automatically by the test harness:

import * as HttpClient from "effect/unstable/http/HttpClient";
import * as HttpBody from "effect/unstable/http/HttpBody";
test(
"PUT and GET round-trip",
Effect.gen(function* () {
const { url } = yield* stack;
const put = yield* HttpClient.put(`${url}/hello.txt`, {
body: HttpBody.text("Hello!"),
});
expect(put.status).toBe(201);
const get = yield* HttpClient.get(`${url}/hello.txt`);
expect(yield* get.text).toBe("Hello!");
}),
);

Tests use the same state management as production deploys. By default, LocalState persists to .alchemy/ so subsequent test runs reuse existing resources (making re-runs fast).

For unit testing providers, Alchemy provides an in-memory state store:

import * as TestState from "alchemy/Test/TestState";
// Start with empty state
const state = TestState.defaultState;
// Or seed with existing resources
const state = TestState.state({
MyResource: {
/* ResourceState */
},
});

The test harness includes a TestCli that auto-approves plans and suppresses interactive prompts. This is provided automatically — you don’t need to configure it.

Alchemy also supports Vitest with the same API:

import { beforeAll, deploy, expect, test } from "alchemy/Test/Vitest";

The Vitest harness provides identical functionality with Vitest’s test runner instead of Bun’s.

The same harness works for offline jobs and streaming pipelines — write to the source, await the sink, no mocks:

// Async / streaming jobs — same pattern.
test("DynamoDB stream → SQS pipeline", Effect.gen(function* () {
const { tableName, queueUrl } = yield* stack;
yield* DynamoDB.putItem({
TableName: tableName,
Item: { id: { S: "1" } },
});
// wait for the Lambda to fan out into the queue
const msg = yield* SQS.receive(queueUrl).pipe(
Effect.timeout("10 seconds"),
);
expect(msg.Body).toContain('"id":"1"');
}));
  • No mocks — Your tests run against real R2, real DynamoDB, real Workers. What passes locally passes in prod.
  • Per-suite isolation — Stage names from PR or test ID. Two suites run the same tests in parallel without collision.
  • Effect-aware test runner — Vitest and Bun test wrapped with Effect support. Yield from any test, get typed errors at the assertion line.