1---
2title: "Part 3: Testing"
3description: Write integration tests that deploy your stack and make HTTP requests against your live Worker.
4sidebar:
5 order: 3
6---
7
8In [Part 2](/cloudflare/tutorial/part-2) you deployed a Worker with R2 Bucket
9bindings. Now you'll write integration tests that deploy the stack,
10hit the live Worker over HTTP, and verify it works.
11
12## Create the test file
13
14Alchemy ships test utilities for Bun that wrap `bun:test` with Effect
15support. Configure providers (and state) once at the top of the file
16with `Test.make({...})` — the same `Cloudflare.providers()` /
17`Cloudflare.state()` you used in your `Stack`:
18
19```typescript
20// test/integ.test.ts
21import * as Cloudflare from "alchemy/Cloudflare";
22import * as Test from "alchemy/Test/Bun";
23import { expect } from "bun:test";
24import * as Effect from "effect/Effect";
25import Stack from "../alchemy.run.ts";
26
27const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
28 providers: Cloudflare.providers(),
29 state: Cloudflare.state(),
30});
31```
32
33`expect` (and any other `bun:test` helpers like `describe`) come
34from `bun:test` directly — `Test.make` only provides the
35Effect-aware test runner pieces.
36
37## Deploy the stack before tests
38
39Use `beforeAll` with `deploy` to deploy your stack once before any
40tests run:
41
42```diff lang="typescript"
43// test/integ.test.ts
44import * as Cloudflare from "alchemy/Cloudflare";
45import * as Test from "alchemy/Test/Bun";
46import { expect } from "bun:test";
47import * as Effect from "effect/Effect";
48import Stack from "../alchemy.run.ts";
49
50const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
51 providers: Cloudflare.providers(),
52 state: Cloudflare.state(),
53});
54
55+const stack = beforeAll(deploy(Stack));
56```
57
58`deploy(Stack)` returns an Effect that plans and applies the stack.
59`beforeAll` runs it once, then returns a lazy accessor you can
60`yield*` inside each test to get the stack outputs.
61
62## Access the stack outputs
63
64Write your first test. Use `yield* stack` to get the outputs you
65returned from your Stack in Part 2:
66
67```diff lang="typescript"
68// test/integ.test.ts
69import * as Cloudflare from "alchemy/Cloudflare";
70import * as Test from "alchemy/Test/Bun";
71import { expect } from "bun:test";
72import * as Effect from "effect/Effect";
73import Stack from "../alchemy.run.ts";
74
75const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
76 providers: Cloudflare.providers(),
77 state: Cloudflare.state(),
78});
79
80const stack = beforeAll(deploy(Stack));
81
82+test(
83+ "worker returns a url",
84+ Effect.gen(function* () {
85+ const { url } = yield* stack;
86+
87+ expect(url).toBeString();
88+ }),
89+);
90```
91
92`test(name, effect)` wraps `bun:test` — you write an Effect generator
93instead of an async function.
94
95## Run the tests
96
97```sh
98bun test test/integ.test.ts
99```
100
101The first run deploys the stack (or reuses the existing one if
102already deployed). Subsequent runs are fast because Alchemy diffs
103and skips unchanged resources.
104
105## Add HTTP assertions
106
107The basic test just checks that a URL exists. Let's verify the Worker
108actually handles requests:
109
110```diff lang="typescript"
111// test/integ.test.ts
112import * as Cloudflare from "alchemy/Cloudflare";
113import * as Test from "alchemy/Test/Bun";
114import { expect } from "bun:test";
115import * as Effect from "effect/Effect";
116+import * as HttpBody from "effect/http/HttpBody";
117+import * as HttpClient from "effect/http/HttpClient";
118import Stack from "../alchemy.run.ts";
119
120const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
121 providers: Cloudflare.providers(),
122 state: Cloudflare.state(),
123});
124
125const stack = beforeAll(deploy(Stack));
126
127-test(
128- "worker returns a url",
129- Effect.gen(function* () {
130- const { url } = yield* stack;
131-
132- expect(url).toBeString();
133- }),
134-);
135+test(
136+ "PUT and GET round-trip an object",
137+ Effect.gen(function* () {
138+ const { url } = yield* stack;
139+
140+ const put = yield* HttpClient.put(`${url}/hello.txt`, {
141+ body: HttpBody.text("Hello, World!"),
142+ });
143+ expect(put.status).toBe(201);
144+
145+ const get = yield* HttpClient.get(`${url}/hello.txt`);
146+ expect(yield* get.text).toBe("Hello, World!");
147+ }),
148+);
149+
150+test(
151+ "GET missing key returns 404",
152+ Effect.gen(function* () {
153+ const { url } = yield* stack;
154+ const response = yield* HttpClient.get(`${url}/no-such-key`);
155+ expect(response.status).toBe(404);
156+ }),
157+);
158```
159
160`HttpClient` is provided automatically by the test harness — no
161extra setup needed.
162
163## Allow the Bucket to be destroyed with objects in it
164
165The PUT test writes `hello.txt` into the Bucket, so the Bucket is no
166longer empty when the tests finish. R2 refuses to delete a Bucket
167that still contains objects, and Alchemy keeps that protection by
168default: destroying a non-empty Bucket fails with `BucketNotEmpty` and
169leaves the Bucket and its objects in place.
170
171This Bucket only ever holds test data, so opt in to deleting its
172objects on destroy with `forceDestroy: true`:
173
174```diff lang="typescript"
175// src/bucket.ts
176import * as Cloudflare from "alchemy/Cloudflare";
177
178-export const Bucket = Cloudflare.R2.Bucket("Bucket");
179+export const Bucket = Cloudflare.R2.Bucket("Bucket", {
180+ forceDestroy: true,
181+});
182```
183
184With `forceDestroy: true`, Alchemy empties the Bucket before deleting
185it. Reserve it for Buckets whose contents are disposable (test
186fixtures, caches, previews); leave it off for Buckets that hold data
187you need to keep.
188
189## Destroy after tests on CI
190
191Right now the stack stays deployed after tests finish. That's great
192locally — you can re-run tests instantly against the already-deployed
193stack. But on CI you want to clean up.
194
195Add `afterAll` with `destroy`, using `skipIf` to only tear down when
196`CI` is set:
197
198```diff lang="typescript"
199import * as Cloudflare from "alchemy/Cloudflare";
200import * as Test from "alchemy/Test/Bun";
201import { expect } from "bun:test";
202import * as Effect from "effect/Effect";
203import * as HttpBody from "effect/http/HttpBody";
204import * as HttpClient from "effect/http/HttpClient";
205import Stack from "../alchemy.run.ts";
206
207const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
208 providers: Cloudflare.providers(),
209 state: Cloudflare.state(),
210});
211
212const stack = beforeAll(deploy(Stack));
213
214+afterAll.skipIf(!process.env.CI)(destroy(Stack));
215
216test(/* .. */);
217
218test(/* .. */);
219```
220
221- **Locally** — `CI` is not set, so `skipIf` skips the destroy. You
222 iterate fast against the live stack.
223- **On CI** — set `CI=true` and the stack is torn down automatically
224 after tests complete.
225
226## Recap
227
228You now have:
229
230- `Test.make({ providers, state })` to wire your provider Layer and
231 state store into the test runner once per file
232- `beforeAll(deploy(Stack))` to deploy once before tests
233- `yield* stack` to access outputs in each test
234- HTTP assertions using Effect's `HttpClient`
235- `forceDestroy: true` so destroy can delete a Bucket that tests wrote to
236- `afterAll.skipIf(!process.env.CI)(destroy(Stack))` for automatic
237 cleanup on CI with fast iteration locally
238
239In [Part 4](/cloudflare/tutorial/part-4), you'll run your stack locally with
240`alchemy dev` for instant feedback during development.