| 1 | --- |
| 2 | title: Test harness |
| 3 | description: Reference for alchemy/Test — every helper, hook, and option exposed by Test.make for Bun and Vitest. |
| 4 | --- |
| 5 | |
| 6 | `alchemy/Test/Bun` and `alchemy/Test/Vitest` expose the same |
| 7 | Effect-aware harness. For the end-to-end walkthrough, see |
| 8 | [Testing a Stack](/testing/testing-a-stack); for provider-lifecycle |
| 9 | testing, see [Testing Providers](/testing/testing-providers). |
| 10 | |
| 11 | ## What Test.make returns |
| 12 | |
| 13 | A single call returns a self-contained API for the file: |
| 14 | |
| 15 | ```typescript |
| 16 | const { test, beforeAll, beforeEach, afterAll, afterEach, deploy, destroy } = |
| 17 | Test.make({ |
| 18 | providers: Cloudflare.providers(), |
| 19 | state: Cloudflare.state(), |
| 20 | }); |
| 21 | ``` |
| 22 | |
| 23 | | Helper | Purpose | |
| 24 | |---|---| |
| 25 | | `test(name, effect)` | Effect-aware test. `HttpClient` and your providers Layer are in scope. | |
| 26 | | `test.skip` / `test.skipIf` / `test.only` / `test.todo` | Skip / focus / todo modifiers (same shape as `bun.test`). | |
| 27 | | `test.provider(name, fn)` | [Provider-lifecycle test](/testing/testing-providers) against a scratch in-memory stack. | |
| 28 | | `beforeAll(effect)` | Run an Effect once. Returns a lazy accessor (`yield* result`) usable inside tests. | |
| 29 | | `beforeEach(effect)` | Run an Effect before every test. | |
| 30 | | `afterAll(effect)` / `afterAll.skipIf(predicate)` | Cleanup hook with conditional teardown. | |
| 31 | | `afterEach(effect)` | Run an Effect after every test. | |
| 32 | | `deploy(Stack, opts?)` | Plan + apply a stack, resolve to its outputs. | |
| 33 | | `destroy(Stack, opts?)` | Plan + apply against an empty desired state. | |
| 34 | |
| 35 | `Test.getWhenReady` / `Test.executeWhenReady` are module-level HTTP |
| 36 | cold-start helpers, taught in [Testing a Stack](/testing/testing-a-stack). |
| 37 | |
| 38 | `expect` (and `describe`) come from the underlying runner — |
| 39 | `bun:test` or `@effect/vitest` — directly. |
| 40 | |
| 41 | ## Test.make options |
| 42 | |
| 43 | ```typescript |
| 44 | Test.make({ |
| 45 | providers, // required |
| 46 | state, // optional |
| 47 | profile, // optional |
| 48 | stage, // optional |
| 49 | dev, // optional — run the suite against local emulators |
| 50 | adopt, // optional |
| 51 | sidecar, // optional — defaults to dev |
| 52 | }); |
| 53 | ``` |
| 54 | |
| 55 | ### `providers` (required) |
| 56 | |
| 57 | The provider Layer that resolves resource implementations. |
| 58 | Usually the same one your `Stack` uses: |
| 59 | |
| 60 | ```typescript |
| 61 | providers: Cloudflare.providers(), |
| 62 | // or merge multiple: |
| 63 | providers: Layer.mergeAll(Cloudflare.providers(), Stripe.providers()), |
| 64 | ``` |
| 65 | |
| 66 | Credentials resolve through the same `AuthProviders` registry as |
| 67 | `alchemy deploy`, so tests pick up stored profiles or |
| 68 | the env-var auth methods registered by each provider. |
| 69 | |
| 70 | ### `state` |
| 71 | |
| 72 | The state store used by top-level `deploy(Stack)` and |
| 73 | `destroy(Stack)` (not by `test.provider`). Defaults to |
| 74 | `localState()` — `.alchemy/` on disk. |
| 75 | |
| 76 | ```typescript |
| 77 | state: Cloudflare.state(), // R2-backed, survives across CI runners |
| 78 | state: localState({ path: ".alchemy-test/" }), // separate dir |
| 79 | state: undefined, // omit → defaults to localState() |
| 80 | ``` |
| 81 | |
| 82 | Persistent state lets `deploy(Stack)` skip recreating unchanged |
| 83 | resources between runs. The run-against-an-existing-stack pattern |
| 84 | built on it lives in [Testing a Stack](/testing/testing-a-stack). |
| 85 | |
| 86 | ### `profile` |
| 87 | |
| 88 | Override `ALCHEMY_PROFILE` for this file only. Useful for |
| 89 | pinning tests to a sandbox profile regardless of what's set in |
| 90 | the environment: |
| 91 | |
| 92 | ```typescript |
| 93 | Test.make({ |
| 94 | providers: AWS.providers(), |
| 95 | profile: "test-sandbox", |
| 96 | }); |
| 97 | ``` |
| 98 | |
| 99 | When omitted, the harness reads `ALCHEMY_PROFILE` from env / `.env` |
| 100 | the same way the CLI does. |
| 101 | |
| 102 | ### `stage` |
| 103 | |
| 104 | Default stage for `deploy(Stack)` / `destroy(Stack)`. Defaults to |
| 105 | `test_$USER` (e.g. `test_sam`) so two people running the same suite |
| 106 | against one account don't collide. Override per file, or per call: |
| 107 | |
| 108 | ```typescript |
| 109 | Test.make({ providers, stage: "ci-pr-42" }); |
| 110 | |
| 111 | // or per-call: |
| 112 | beforeAll(deploy(Stack, { stage: "ci-pr-42" })); |
| 113 | afterAll.skipIf(!process.env.CI)(destroy(Stack, { stage: "ci-pr-42" })); |
| 114 | ``` |
| 115 | |
| 116 | A unique stage per PR or test run lets multiple suites run in |
| 117 | parallel against the same provider account without colliding. |
| 118 | |
| 119 | ### `dev` |
| 120 | |
| 121 | Off by default — tests deploy to the real cloud. `dev: true` runs |
| 122 | the whole file in local-dev mode, the same wiring as |
| 123 | [`alchemy dev`](/environments/local-development): Cloudflare |
| 124 | Workers, Durable Objects, KV, R2, D1, Queues, and Workflows run in |
| 125 | workerd with local simulators, and AWS Lambda/ECS plus the |
| 126 | emulated AWS surface run in a local Docker emulator. No cloud |
| 127 | account is touched for emulated resources. |
| 128 | |
| 129 | ```typescript |
| 130 | Test.make({ |
| 131 | providers: Cloudflare.providers(), |
| 132 | dev: true, // deploy(Stack) boots workerd / Docker simulators |
| 133 | }); |
| 134 | ``` |
| 135 | |
| 136 | Everything else in the file is unchanged — `beforeAll(deploy(Stack))` |
| 137 | boots the stack locally and the outputs (`url`, ids) point at the |
| 138 | local instances, so the same HTTP assertions run against |
| 139 | `http://localhost:<port>`. Local resource ids are `dev:`-prefixed, |
| 140 | which doubles as proof no cloud call ran. `Alchemy.remote()` still |
| 141 | pins individual resources live, exactly as it does under |
| 142 | `alchemy dev`. |
| 143 | |
| 144 | When omitted, the flag falls back to the `ALCHEMY_DEV` env var — |
| 145 | leave it out of the file and set `ALCHEMY_DEV=1` locally to run the |
| 146 | same suite against emulators while CI runs it live. Separately, |
| 147 | `ALCHEMY_TEST_DEV=1` **overrides** the option in both directions — |
| 148 | use it to force an entire existing suite through local providers |
| 149 | without editing each `Test.make`. |
| 150 | |
| 151 | What each cloud emulates is covered in |
| 152 | [Cloudflare local development](/cloudflare/local-development) and |
| 153 | [AWS local development](/aws/local-development); the step-by-step |
| 154 | walkthrough is [Cloudflare Tutorial Part 4](/cloudflare/tutorial/part-4). |
| 155 | |
| 156 | ### `adopt` |
| 157 | |
| 158 | Engine-level adoption policy for the run, matching the CLI's |
| 159 | `--adopt` flag. When `true`, a resource with no prior state whose |
| 160 | physical counterpart already exists in the cloud is adopted via |
| 161 | `provider.read` instead of failing. Defaults to `false`. |
| 162 | |
| 163 | ### `sidecar` |
| 164 | |
| 165 | Only meaningful in dev mode, and defaults to the resolved `dev` |
| 166 | flag: dev tests run local providers behind the same RPC sidecar |
| 167 | process the real `alchemy dev` command uses. `sidecar: false` runs |
| 168 | them in-process instead — useful when debugging provider code, |
| 169 | since there's no child process between you and the breakpoint. |
| 170 | |
| 171 | ## Hooks |
| 172 | |
| 173 | ### `beforeAll(effect) → Effect.Effect<A>` |
| 174 | |
| 175 | Runs the Effect once before any test in the file. Stores the |
| 176 | result and returns a lazy accessor — `yield* accessor` inside |
| 177 | any test or other hook returns the resolved value: |
| 178 | |
| 179 | ```typescript |
| 180 | const stack = beforeAll(deploy(Stack)); |
| 181 | const seed = beforeAll(Effect.gen(function* () { |
| 182 | yield* DynamoDB.putItem({ /* ... */ }); |
| 183 | return Date.now(); |
| 184 | })); |
| 185 | |
| 186 | test( |
| 187 | "uses both", |
| 188 | Effect.gen(function* () { |
| 189 | const { url } = yield* stack; |
| 190 | const startedAt = yield* seed; |
| 191 | /* ... */ |
| 192 | }), |
| 193 | ); |
| 194 | ``` |
| 195 | |
| 196 | Default timeout is **120s**. Override with the second argument: |
| 197 | |
| 198 | ```typescript |
| 199 | beforeAll(deploy(Stack), { timeout: 300_000 }); |
| 200 | ``` |
| 201 | |
| 202 | ### `beforeEach(effect)` |
| 203 | |
| 204 | Runs the Effect before every test. No accessor returned — for |
| 205 | side-effect setup only (truncate a table, reset a feature flag, |
| 206 | …). |
| 207 | |
| 208 | ### `afterAll(effect)` and `afterAll.skipIf(predicate)` |
| 209 | |
| 210 | Cleanup hook with conditional teardown: |
| 211 | |
| 212 | ```typescript |
| 213 | afterAll(destroy(Stack)); // always destroy |
| 214 | afterAll.skipIf(!process.env.CI)(destroy(Stack)); // CI only |
| 215 | afterAll.skipIf(true)(destroy(Stack)); // never (debugging) |
| 216 | ``` |
| 217 | |
| 218 | `afterAll.skipIf(true)` short-circuits without registering a |
| 219 | hook at all — there's no risk of an `Effect` being constructed |
| 220 | and dropped. |
| 221 | |
| 222 | ### `afterEach(effect)` |
| 223 | |
| 224 | Runs after every test. Combine with `beforeEach` for |
| 225 | test-isolated fixtures. |
| 226 | |
| 227 | ## Test variants |
| 228 | |
| 229 | ```typescript |
| 230 | test.skip("not ready yet", Effect.gen(function* () { /* ... */ })); |
| 231 | |
| 232 | test.skipIf(process.env.CI)( |
| 233 | "local-only smoke test", |
| 234 | Effect.gen(function* () { /* ... */ }), |
| 235 | ); |
| 236 | |
| 237 | test.only( |
| 238 | "the one I'm debugging", |
| 239 | Effect.gen(function* () { /* ... */ }), |
| 240 | ); |
| 241 | |
| 242 | test.todo("backfill once R2 has multipart helper"); |
| 243 | ``` |
| 244 | |
| 245 | `test.provider` mirrors the same shape (semantics in |
| 246 | [Testing Providers](/testing/testing-providers)): |
| 247 | |
| 248 | ```typescript |
| 249 | test.provider.skip(name, fn); |
| 250 | test.provider.skipIf(condition)(name, fn); |
| 251 | ``` |
| 252 | |
| 253 | :::note[Vitest differences] |
| 254 | On Vitest, `test.only` is supported but `test.todo` is a stub |
| 255 | (maps to `it.todo`). `test.skip` / `test.skipIf` work identically. |
| 256 | ::: |
| 257 | |
| 258 | ## HttpClient is in scope |
| 259 | |
| 260 | `HttpClient` is wired into every `test` Effect, so you can call |
| 261 | it directly: |
| 262 | |
| 263 | ```typescript |
| 264 | import * as HttpClient from "effect/http/HttpClient"; |
| 265 | |
| 266 | test( |
| 267 | "health check", |
| 268 | Effect.gen(function* () { |
| 269 | const { url } = yield* stack; |
| 270 | const res = yield* HttpClient.get(`${url}/health`); |
| 271 | expect(res.status).toBe(200); |
| 272 | }), |
| 273 | ); |
| 274 | ``` |
| 275 | |
| 276 | The implementation comes from |
| 277 | `effect/http/FetchHttpClient` — same client the CLI |
| 278 | uses. For a full PUT/GET round-trip against a deployed stack, |
| 279 | see [Testing a Stack → Drive the live URL](/testing/testing-a-stack). |
| 280 | |
| 281 | ## Bun vs Vitest |
| 282 | |
| 283 | The two adapters expose the **same API**: |
| 284 | |
| 285 | ```diff lang="typescript" |
| 286 | -import * as Test from "alchemy/Test/Bun"; |
| 287 | -import { expect } from "bun:test"; |
| 288 | +import * as Test from "alchemy/Test/Vitest"; |
| 289 | +import { expect } from "@effect/vitest"; |
| 290 | |
| 291 | const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ |
| 292 | providers: Cloudflare.providers(), |
| 293 | state: Cloudflare.state(), |
| 294 | }); |
| 295 | ``` |
| 296 | |
| 297 | - **Bun** uses `bun:test` directly. Every `test(...)` becomes a |
| 298 | `bun.test(...)` call wrapped with `Effect.runPromise`. |
| 299 | - **Vitest** uses `@effect/vitest`'s `it.live`, so Effect-aware |
| 300 | tests stay first-class. Default hook timeout is the same |
| 301 | (120s). |
| 302 | |
| 303 | Pick whichever runner your project already uses; nothing in the |
| 304 | test code changes. |
| 305 | |
| 306 | ## Where next |
| 307 | |
| 308 | - [Testing a Stack](/testing/testing-a-stack) — deploy once, drive the live URL, tear down. |
| 309 | - [Testing Providers](/testing/testing-providers) — exercise create / update / replace / delete with `test.provider`. |
| 310 | - [Testing](/testing) — the testing overview. |
| 311 | - [State Store](/state-store) — choosing between `localState()`, `Cloudflare.state()`, and friends. |
| 312 | - [Profiles](/environments/profiles) — how `ALCHEMY_PROFILE` and the `profile` factory option resolve credentials. |