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});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 alchemy login 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". 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.
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.