Test harness
Test.make is the same Effect-aware harness on three runners:
alchemy-test, bun test, and Vitest. 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 runner directly —
@alchemy.run/test, bun:test, or @effect/vitest.
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. ALCHEMY_TEST_STAGE replaces
the default for the whole run
(from the command line),
e.g. one stage per git worktree. 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/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/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.
Runners
Section titled “Runners”@alchemy.run/test
holds Test.make for every runner. Install it once, whichever runner
you use:
pnpm add -D https://pkg.alchemy.run/@alchemy.run/test/pr:2930:a10573ebun add -d https://pkg.alchemy.run/@alchemy.run/test/pr:2930:a10573enpm install -D https://pkg.alchemy.run/@alchemy.run/test/pr:2930:a10573eyarn add -D https://pkg.alchemy.run/@alchemy.run/test/pr:2930:a10573eEach runner has its own adapter in the package. The Test.make API
is identical on all three; only the import and the command change.
alchemy-test
Section titled “alchemy-test”alchemy-test is the runner Alchemy’s own suite uses, and it ships
in the same package. It runs every test file in one bun process with
files and tests in flight at once, which suits slow tests that wait
on real clouds.
Import the harness from @alchemy.run/test/Alchemy and expect from
@alchemy.run/test.
import * as Test from "@alchemy.run/test/Alchemy";import { expect } from "@alchemy.run/test";Run it with the alchemy-test binary. It picks up every
*.test.ts under ./test, or the paths you pass.
pnpm alchemy-testbun alchemy-testnpx alchemy-testyarn alchemy-testpnpm alchemy-test test/integ.test.ts -t "serves the url"bun alchemy-test test/integ.test.ts -t "serves the url"npx alchemy-test test/integ.test.ts -t "serves the url"yarn alchemy-test test/integ.test.ts -t "serves the url"Flags set the environment before any test file is imported:
--profile <name> sets ALCHEMY_PROFILE, --stage <name> sets
ALCHEMY_TEST_STAGE, and --fast sets FAST=1.
pnpm alchemy-test --profile testing --stage test_sam_feat-xbun alchemy-test --profile testing --stage test_sam_feat-xnpx alchemy-test --profile testing --stage test_sam_feat-xyarn alchemy-test --profile testing --stage test_sam_feat-xPassing tests stay quiet; a failing test prints its error and
captured output inline. Every test’s output goes to a per-run log
under .alchemy/log/test/, whose path is printed at the end. Add
--tui for a live view in an interactive terminal.
bun test
Section titled “bun test”Import the harness from @alchemy.run/test/Bun and expect from
bun:test.
import * as Test from "@alchemy.run/test/Bun";import { expect } from "bun:test";Run it with bun’s built-in runner. Every test(...) becomes a
bun:test test that runs its Effect.
bun test test/integ.test.tsVitest
Section titled “Vitest”Install vitest and @effect/vitest next to @alchemy.run/test.
npm add -D https://pkg.alchemy.run/@alchemy.run/test/pr:2930:a10573e vitest @effect/vitestImport the harness from @alchemy.run/test/Vitest and expect from
@effect/vitest.
import * as Test from "@alchemy.run/test/Vitest";import { expect } from "@effect/vitest";Every test(...) runs as an @effect/vitest it.live test, with
the same 120s default hook timeout.
pnpm vitest run test/integ.test.tsbun vitest run test/integ.test.tsnpx vitest run test/integ.test.tsyarn vitest run test/integ.test.tsSetting the stage from the command line
Section titled “Setting the stage from the command line”The default stage is test_$USER on every runner. ALCHEMY_TEST_STAGE
replaces it for the whole run. alchemy-test sets it with
--stage; with bun test or Vitest, set the variable yourself.
pnpm alchemy-test --stage ci-pr-42ALCHEMY_TEST_STAGE=ci-pr-42 bun testALCHEMY_TEST_STAGE=ci-pr-42 pnpm vitest runbun alchemy-test --stage ci-pr-42ALCHEMY_TEST_STAGE=ci-pr-42 bun testALCHEMY_TEST_STAGE=ci-pr-42 bun vitest runnpx alchemy-test --stage ci-pr-42ALCHEMY_TEST_STAGE=ci-pr-42 bun testALCHEMY_TEST_STAGE=ci-pr-42 npx vitest runyarn alchemy-test --stage ci-pr-42ALCHEMY_TEST_STAGE=ci-pr-42 bun testALCHEMY_TEST_STAGE=ci-pr-42 yarn vitest runA stage passed to Test.make or to a single deploy still wins
over the variable.
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.