Testing a Stack
This is the end-to-end pattern for integration-testing a deployed Stack against the real cloud: deploy once in beforeAll, drive the live URL from Effect-aware tests, destroy (or don’t) in afterAll. The harness API is documented at Test harness, the model at Testing, and the cloud-specific step-by-steps are Cloudflare Tutorial Part 3 and AWS Tutorial Part 3.
Set up the harness
Section titled “Set up the harness”Configure Providers and state once per file — Test.make returns everything the file needs:
import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Drizzle from "alchemy/Drizzle";import * as Neon from "alchemy/Neon";import * as Test from "alchemy/Test/Bun";import { expect } from "bun:test";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import Stack from "../alchemy.run.ts";
const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ providers: Layer.mergeAll( Cloudflare.providers(), Drizzle.providers(), Neon.providers(), ), state: Alchemy.localState(),});Bun vs Vitest is a two-line import swap — see Bun vs Vitest.
Deploy once with beforeAll
Section titled “Deploy once with beforeAll”beforeAll(deploy(Stack)) deploys once for the whole file and returns a lazy accessor every test can yield*:
const stack = beforeAll(deploy(Stack));
test( "worker returns a url", Effect.gen(function* () { const { url } = yield* stack;
expect(url).toBeString(); }),);The default hook timeout is 120s — give slow stacks more room:
// This stack deploys a Container whose image build + push can take// well over the default 120s hook budget.const stack = beforeAll(deploy(Stack), { timeout: 600_000 });beforeAll accessors compose — seed data in a second beforeAll that tests yield* alongside the stack; see Hooks.
Run it
Section titled “Run it”Run the suite with your runner:
bun test test/integ.test.tsThe first run deploys; re-runs diff and skip unchanged Resources, and tests default to the isolated test stage so they never clobber your dev deployment.
Drive the live URL
Section titled “Drive the live URL”HttpClient is already in scope in every test Effect:
import * as HttpBody from "effect/unstable/http/HttpBody";import * as HttpClient from "effect/unstable/http/HttpClient";
test( "PUT and GET round-trip an object", Effect.gen(function* () { const { url } = yield* stack;
const put = yield* HttpClient.put(`${url}/hello.txt`, { body: HttpBody.text("Hello, World!"), }); expect(put.status).toBe(201);
const get = yield* HttpClient.get(`${url}/hello.txt`); expect(yield* get.text).toBe("Hello, World!"); }),);How the client is wired is covered in Test harness.
Retry the first request
Section titled “Retry the first request”Fresh workers.dev URLs and Lambda Function URLs transiently 404/5xx while routes, bindings, DNS, and IAM propagate — ride out the cold-start window on the first request of each test:
const { getWhenReady } = Test;
test( "worker answers once the edge converges", Effect.gen(function* () { const { url } = yield* stack;
const response = yield* getWhenReady(url); expect(response.status).toBe(200); }),);getWhenReady (and executeWhenReady for arbitrary requests) retries only 404/5xx — 20 attempts by default, exponential from 500ms — so deliberate 400/401/403 assertions observe those statuses immediately. For arbitrary effects, use a bounded manual retry:
import * as Schedule from "effect/Schedule";
const response = yield* HttpClient.get(`${url}/health`).pipe( Effect.retry({ schedule: Schedule.exponential("500 millis"), times: 10 }),);Poll async effects
Section titled “Poll async effects”Queues drain, workflows complete, and crons fire asynchronously — poll with a bounded declarative schedule, never a deadline loop:
const status = yield* HttpClient.get(`${url}/workflow/status/${instanceId}`).pipe( Effect.flatMap((res) => res.json), Effect.map((body) => body as { status: string }), Effect.repeat({ schedule: Schedule.spaced("2 seconds"), until: (s) => s.status === "complete", times: 30, }),);Bounded times fails fast instead of leaking into the runner timeout; while (Date.now() < deadline) loops ignore interruption.
Tear down — or don’t
Section titled “Tear down — or don’t”Destroy on CI, keep the Stack alive locally for fast iteration:
afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); // keep alive with NO_DESTROY=1afterAll.skipIf(!process.env.CI)(destroy(Stack)); // destroy on CI onlyLocally, the deployed Stack plus cached state make re-runs near-instant. Hook semantics live in Hooks.
Run against an existing deployment
Section titled “Run against an existing deployment”Already ran alchemy deploy and just want to hit the URL? Swap the deploy for a read of the outputs:
const stack = beforeAll( process.env.SKIP_DEPLOY ? Effect.succeed({ url: process.env.STACK_URL! }) : deploy(Stack),);Share one Stack across files
Section titled “Share one Stack across files”Give every file’s Test.make the same remote state and the same stage — identical state + stage means the second file’s deploy(Stack) is a no-op diff:
// test/api.integ.test.ts AND test/queue.integ.test.tsconst { test, beforeAll, afterAll, deploy, destroy } = Test.make({ providers: Cloudflare.providers(), state: Cloudflare.state(), // remote, shared across files and runners stage: "test", // same stage → same Stack instance});See State store for choosing a remote store.
Give every PR its own stage so suites run in parallel against one account:
const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ providers: Cloudflare.providers(), stage: `pr-${process.env.PR_NUMBER}`,});Harness deploy/destroy never prompt for approval — --yes is only for CI workflows invoking the CLI (alchemy deploy --stage pr-42 --yes); see Stage and the CI guide for full workflow YAML.
Where next
Section titled “Where next”- Testing — the model behind the harness.
- Test harness — every helper, hook, and option on
Test.make. - Testing Providers — lifecycle-testing a custom provider with
test.provider. - CI — running these suites in CI with per-PR stages.
- Cloudflare Tutorial Part 3 / AWS Tutorial Part 3 — your first integration test, step by step.
- State store — local vs remote state.
- Stages — isolating deployments per stage.