Skip to content

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.

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(),
});
HelperPurpose
test(name, effect)Effect-aware test. HttpClient and your providers Layer are in scope.
test.skip / test.skipIf / test.only / test.todoSkip / 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({
providers, // required
state, // optional
profile, // optional
stage, // optional
});

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 runners
state: localState({ path: ".alchemy-test/" }), // separate dir
state: 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.

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.

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 });

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 destroy
afterAll.skipIf(!process.env.CI)(destroy(Stack)); // CI only
afterAll.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.

Runs after every test. Combine with beforeEach for test-isolated fixtures.

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 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.

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:test directly. Every test(...) becomes a bun.test(...) call wrapped with Effect.runPromise.
  • Vitest uses @effect/vitest’s it.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.

  • 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_PROFILE and the profile factory option resolve credentials.