Test harness
alchemy/Test/Bun and alchemy/Test/Vitest expose the same
Effect-aware harness. For the end-to-end walkthrough, see
Testing a Stack; for provider-lifecycle
testing, see Testing Providers.
What Test.make returns
Section titled “What Test.make returns”A single call returns a self-contained API for the file:
const { test, beforeAll, beforeEach, afterAll, afterEach, deploy, destroy } = Test.make({ providers: Cloudflare.providers(), state: Cloudflare.state(), });| Helper | Purpose |
|---|---|
test(name, effect) |
Effect-aware test. HttpClient and your providers Layer are in scope. |
test.skip / test.skipIf / test.only / test.todo |
Skip / focus / todo modifiers (same shape as bun.test). |
test.provider(name, fn) |
Provider-lifecycle test against a scratch in-memory stack. |
beforeAll(effect) |
Run an Effect once. Returns a lazy accessor (yield* result) usable inside tests. |
beforeEach(effect) |
Run an Effect before every test. |
afterAll(effect) / afterAll.skipIf(predicate) |
Cleanup hook with conditional teardown. |
afterEach(effect) |
Run an Effect after every test. |
deploy(Stack, opts?) |
Plan + apply a stack, resolve to its outputs. |
destroy(Stack, opts?) |
Plan + apply against an empty desired state. |
Test.getWhenReady / Test.executeWhenReady are module-level HTTP
cold-start helpers, taught in Testing a Stack.
expect (and describe) come from the underlying runner —
bun:test or @effect/vitest — directly.
Test.make options
Section titled “Test.make options”Test.make({ providers, // required state, // optional profile, // optional stage, // optional dev, // optional — run the suite against local emulators adopt, // optional sidecar, // optional — defaults to dev});providers (required)
Section titled “providers (required)”The provider Layer that resolves resource implementations.
Usually the same one your Stack uses:
providers: Cloudflare.providers(),// or merge multiple:providers: Layer.mergeAll(Cloudflare.providers(), Stripe.providers()),Credentials resolve through the same AuthProviders registry as
alchemy deploy, so tests pick up stored profiles or
the env-var auth methods registered by each provider.
The state store used by top-level deploy(Stack) and
destroy(Stack) (not by test.provider). Defaults to
localState() — .alchemy/ on disk.
state: Cloudflare.state(), // R2-backed, survives across CI runnersstate: localState({ path: ".alchemy-test/" }), // separate dirstate: undefined, // omit → defaults to localState()Persistent state lets deploy(Stack) skip recreating unchanged
resources between runs. The run-against-an-existing-stack pattern
built on it lives in Testing a Stack.
profile
Section titled “profile”Override ALCHEMY_PROFILE for this file only. Useful for
pinning tests to a sandbox profile regardless of what’s set in
the environment:
Test.make({ providers: AWS.providers(), profile: "test-sandbox",});When omitted, the harness reads ALCHEMY_PROFILE from env / .env
the same way the CLI does.
Default stage for deploy(Stack) / destroy(Stack). Defaults to
test_$USER (e.g. test_sam) so two people running the same suite
against one account don’t collide. Override per file, or per call:
Test.make({ providers, stage: "ci-pr-42" });
// or per-call:beforeAll(deploy(Stack, { stage: "ci-pr-42" }));afterAll.skipIf(!process.env.CI)(destroy(Stack, { stage: "ci-pr-42" }));A unique stage per PR or test run lets multiple suites run in parallel against the same provider account without colliding.
Off by default — tests deploy to the real cloud. dev: true runs
the whole file in local-dev mode, the same wiring as
alchemy dev: Cloudflare
Workers, Durable Objects, KV, R2, D1, Queues, and Workflows run in
workerd with local simulators, and AWS Lambda/ECS plus the
emulated AWS surface run in a local Docker emulator. No cloud
account is touched for emulated resources.
Test.make({ providers: Cloudflare.providers(), dev: true, // deploy(Stack) boots workerd / Docker simulators});Everything else in the file is unchanged — beforeAll(deploy(Stack))
boots the stack locally and the outputs (url, ids) point at the
local instances, so the same HTTP assertions run against
http://localhost:<port>. Local resource ids are dev:-prefixed,
which doubles as proof no cloud call ran. Alchemy.remote() still
pins individual resources live, exactly as it does under
alchemy dev.
When omitted, the flag falls back to the ALCHEMY_DEV env var —
leave it out of the file and set ALCHEMY_DEV=1 locally to run the
same suite against emulators while CI runs it live. Separately,
ALCHEMY_TEST_DEV=1 overrides the option in both directions —
use it to force an entire existing suite through local providers
without editing each Test.make.
What each cloud emulates is covered in Cloudflare local development and AWS local development; the step-by-step walkthrough is Cloudflare Tutorial Part 4.
Engine-level adoption policy for the run, matching the CLI’s
--adopt flag. When true, a resource with no prior state whose
physical counterpart already exists in the cloud is adopted via
provider.read instead of failing. Defaults to false.
sidecar
Section titled “sidecar”Only meaningful in dev mode, and defaults to the resolved dev
flag: dev tests run local providers behind the same RPC sidecar
process the real alchemy dev command uses. sidecar: false runs
them in-process instead — useful when debugging provider code,
since there’s no child process between you and the breakpoint.
beforeAll(effect) → Effect.Effect<A>
Section titled “beforeAll(effect) → Effect.Effect<A>”Runs the Effect once before any test in the file. Stores the
result and returns a lazy accessor — yield* accessor inside
any test or other hook returns the resolved value:
const stack = beforeAll(deploy(Stack));const seed = beforeAll(Effect.gen(function* () { yield* DynamoDB.putItem({ /* ... */ }); return Date.now();}));
test( "uses both", Effect.gen(function* () { const { url } = yield* stack; const startedAt = yield* seed; /* ... */ }),);Default timeout is 120s. Override with the second argument:
beforeAll(deploy(Stack), { timeout: 300_000 });beforeEach(effect)
Section titled “beforeEach(effect)”Runs the Effect before every test. No accessor returned — for side-effect setup only (truncate a table, reset a feature flag, …).
afterAll(effect) and afterAll.skipIf(predicate)
Section titled “afterAll(effect) and afterAll.skipIf(predicate)”Cleanup hook with conditional teardown:
afterAll(destroy(Stack)); // always destroyafterAll.skipIf(!process.env.CI)(destroy(Stack)); // CI onlyafterAll.skipIf(true)(destroy(Stack)); // never (debugging)afterAll.skipIf(true) short-circuits without registering a
hook at all — there’s no risk of an Effect being constructed
and dropped.
afterEach(effect)
Section titled “afterEach(effect)”Runs after every test. Combine with beforeEach for
test-isolated fixtures.
Test variants
Section titled “Test variants”test.skip("not ready yet", Effect.gen(function* () { /* ... */ }));
test.skipIf(process.env.CI)( "local-only smoke test", Effect.gen(function* () { /* ... */ }),);
test.only( "the one I'm debugging", Effect.gen(function* () { /* ... */ }),);
test.todo("backfill once R2 has multipart helper");test.provider mirrors the same shape (semantics in
Testing Providers):
test.provider.skip(name, fn);test.provider.skipIf(condition)(name, fn);HttpClient is in scope
Section titled “HttpClient is in scope”HttpClient is wired into every test Effect, so you can call
it directly:
import * as HttpClient from "effect/unstable/http/HttpClient";
test( "health check", Effect.gen(function* () { const { url } = yield* stack; const res = yield* HttpClient.get(`${url}/health`); expect(res.status).toBe(200); }),);The implementation comes from
effect/unstable/http/FetchHttpClient — same client the CLI
uses. For a full PUT/GET round-trip against a deployed stack,
see Testing a Stack → Drive the live URL.
Bun vs Vitest
Section titled “Bun vs Vitest”The two adapters expose the same API:
import * as Test from "alchemy/Test/Bun";import { expect } from "bun:test";import * as Test from "alchemy/Test/Vitest";import { expect } from "@effect/vitest";
const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ providers: Cloudflare.providers(), state: Cloudflare.state(),});- Bun uses
bun:testdirectly. Everytest(...)becomes abun.test(...)call wrapped withEffect.runPromise. - Vitest uses
@effect/vitest’sit.live, so Effect-aware tests stay first-class. Default hook timeout is the same (120s).
Pick whichever runner your project already uses; nothing in the test code changes.
Where next
Section titled “Where next”- Testing a Stack — deploy once, drive the live URL, tear down.
- Testing Providers — exercise create / update / replace / delete with
test.provider. - Testing — the testing overview.
- State Store — choosing between
localState(),Cloudflare.state(), and friends. - Profiles — how
ALCHEMY_PROFILEand theprofilefactory option resolve credentials.