1---
2title: Testing a Stack
3description: Deploy your real Stack once per suite, drive it over HTTP, tear it down.
4---
5
6This 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](/testing/test-harness), the model at [Testing](/testing), and the cloud-specific step-by-steps are [Cloudflare Tutorial Part 3](/cloudflare/tutorial/part-3) and [AWS Tutorial Part 3](/aws/tutorial/part-3).
7
8## Set up the harness
9
10Configure Providers and state once per file — `Test.make` returns everything the file needs:
11
12```typescript
13import * as Alchemy from "alchemy";
14import * as Cloudflare from "alchemy/Cloudflare";
15import * as Drizzle from "alchemy/Drizzle";
16import * as Neon from "alchemy/Neon";
17import * as Test from "alchemy/Test/Bun";
18import { expect } from "bun:test";
19import * as Effect from "effect/Effect";
20import * as Layer from "effect/Layer";
21import Stack from "../alchemy.run.ts";
22
23const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
24 providers: Layer.mergeAll(
25 Cloudflare.providers(),
26 Drizzle.providers(),
27 Neon.providers(),
28 ),
29 state: Alchemy.localState(),
30});
31```
32
33Bun vs Vitest is a two-line import swap — see [Bun vs Vitest](/testing/test-harness#bun-vs-vitest).
34
35## Deploy once with beforeAll
36
37`beforeAll(deploy(Stack))` deploys once for the whole file and returns a lazy accessor every test can `yield*`:
38
39```typescript
40const stack = beforeAll(deploy(Stack));
41
42test(
43 "worker returns a url",
44 Effect.gen(function* () {
45 const { url } = yield* stack;
46
47 expect(url).toBeString();
48 }),
49);
50```
51
52The default hook timeout is 120s — give slow stacks more room:
53
54```typescript
55// This stack deploys a Container whose image build + push can take
56// well over the default 120s hook budget.
57const stack = beforeAll(deploy(Stack), { timeout: 600_000 });
58```
59
60`beforeAll` accessors compose — seed data in a second `beforeAll` that tests `yield*` alongside the stack; see [Hooks](/testing/test-harness#hooks).
61
62## Run it
63
64Run the suite with your runner:
65
66```sh
67bun test test/integ.test.ts
68```
69
70The first run deploys; re-runs diff and skip unchanged Resources, and tests default to the isolated `test_$USER` [stage](/environments/stages) so they never clobber your `live_$USER` / `dev_$USER` deployments.
71
72## Run it locally with dev mode
73
74The same suite can run entirely on your machine. `dev: true` gives the file [`alchemy dev`](/environments/local-development) semantics — Workers, Durable Objects, KV, R2, D1, Queues, and Workflows run in workerd, AWS Lambda and its emulated services run in Docker — so `deploy(Stack)` boots local simulators and the `url` output points at `http://localhost:<port>`:
75
76```typescript
77const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
78 providers: Cloudflare.providers(),
79 dev: true,
80});
81```
82
83Every assertion below works unchanged against the local stack. To keep one file that runs locally on your laptop and live in CI, omit the flag and set `ALCHEMY_DEV=1` in your shell instead — see [Test harness → dev](/testing/test-harness#dev) for the full semantics and [Tutorial Part 4](/cloudflare/tutorial/part-4) for the walkthrough.
84
85## Drive the live URL
86
87`HttpClient` is already in scope in every test Effect:
88
89```typescript
90import * as HttpBody from "effect/http/HttpBody";
91import * as HttpClient from "effect/http/HttpClient";
92
93test(
94 "PUT and GET round-trip an object",
95 Effect.gen(function* () {
96 const { url } = yield* stack;
97
98 const put = yield* HttpClient.put(`${url}/hello.txt`, {
99 body: HttpBody.text("Hello, World!"),
100 });
101 expect(put.status).toBe(201);
102
103 const get = yield* HttpClient.get(`${url}/hello.txt`);
104 expect(yield* get.text).toBe("Hello, World!");
105 }),
106);
107```
108
109How the client is wired is covered in [Test harness](/testing/test-harness#httpclient-is-in-scope).
110
111## Retry the first request
112
113Fresh `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:
114
115```typescript
116const { getWhenReady } = Test;
117
118test(
119 "worker answers once the edge converges",
120 Effect.gen(function* () {
121 const { url } = yield* stack;
122
123 const response = yield* getWhenReady(url);
124 expect(response.status).toBe(200);
125 }),
126);
127```
128
129`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:
130
131```typescript
132import * as Schedule from "effect/Schedule";
133
134const response = yield* HttpClient.get(`${url}/health`).pipe(
135 Effect.retry({ schedule: Schedule.exponential("500 millis"), times: 10 }),
136);
137```
138
139## Poll async effects
140
141Queues drain, workflows complete, and crons fire asynchronously — poll with a bounded declarative schedule, never a deadline loop:
142
143```typescript
144const status = yield* HttpClient.get(`${url}/workflow/status/${instanceId}`).pipe(
145 Effect.flatMap((res) => res.json),
146 Effect.map((body) => body as { status: string }),
147 Effect.repeat({
148 schedule: Schedule.spaced("2 seconds"),
149 until: (s) => s.status === "complete",
150 times: 30,
151 }),
152);
153```
154
155Bounded `times` fails fast instead of leaking into the runner timeout; `while (Date.now() < deadline)` loops ignore interruption.
156
157## Tear down — or don't
158
159Destroy on CI, keep the Stack alive locally for fast iteration:
160
161```typescript
162afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); // keep alive with NO_DESTROY=1
163afterAll.skipIf(!process.env.CI)(destroy(Stack)); // destroy on CI only
164```
165
166Locally, the deployed Stack plus cached state make re-runs near-instant. Hook semantics live in [Hooks](/testing/test-harness#hooks).
167
168## Run against an existing deployment
169
170Already ran `alchemy deploy` and just want to hit the URL? Swap the deploy for a read of the outputs:
171
172```typescript
173const stack = beforeAll(
174 process.env.SKIP_DEPLOY
175 ? Effect.succeed({ url: process.env.STACK_URL! })
176 : deploy(Stack),
177);
178```
179
180## Share one Stack across files
181
182Give 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. The default `test_$USER` stage already does this for a single developer; pin an explicit stage when files must share a stack across users or CI jobs:
183
184```typescript
185// test/api.integ.test.ts AND test/queue.integ.test.ts
186const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
187 providers: Cloudflare.providers(),
188 state: Cloudflare.state(), // remote, shared across files and runners
189 stage: "shared", // same stage → same Stack instance
190});
191```
192
193See [State store](/state-store) for choosing a remote store.
194
195## CI
196
197Give every PR its own stage so suites run in parallel against one account:
198
199```typescript
200const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
201 providers: Cloudflare.providers(),
202 stage: `pr-${process.env.PR_NUMBER}`,
203});
204```
205
206Harness `deploy`/`destroy` never prompt for approval — `--yes` is only for CI workflows invoking the CLI (`alchemy deploy --stage pr-42 --yes`); see [Stage](/testing/test-harness#stage) and the [CI guide](/environments/ci) for full workflow YAML.
207
208## Where next
209
210- [Testing](/testing) — the model behind the harness.
211- [Test harness](/testing/test-harness) — every helper, hook, and option on `Test.make`.
212- [Testing Providers](/testing/testing-providers) — lifecycle-testing a custom provider with `test.provider`.
213- [CI](/environments/ci) — running these suites in CI with per-PR stages.
214- [Cloudflare Tutorial Part 3](/cloudflare/tutorial/part-3) / [AWS Tutorial Part 3](/aws/tutorial/part-3) — your first integration test, step by step.
215- [State store](/state-store) — local vs remote state.
216- [Stages](/environments/stages) — isolating deployments per stage.