Skip to content

Part 3: Testing

In Part 2 you deployed a Worker with R2 Bucket bindings. Now you’ll write integration tests that deploy the stack, hit the live Worker over HTTP, and verify it works.

Alchemy ships test utilities for Bun that wrap bun:test with Effect support. Configure providers (and state) once at the top of the file with Test.make({...}) — the same Cloudflare.providers() / Cloudflare.state() you used in your Stack:

test/integ.test.ts
import * as
import Cloudflare
Cloudflare
from "alchemy/Cloudflare";
import * as
import Test
Test
from "alchemy/Test/Bun";
import {
const expect: Expect

Asserts that a value matches some criteria.

@linkhttps://jestjs.io/docs/expect#reference

@example expect(1 + 1).toBe(2); expect([1,2,3]).toContain(2); expect(null).toBeNull();

@paramactual The actual (received) value

expect
} from "bun:test";
import * as
import Effect
Effect
from "effect/Effect";
import
import Stack
Stack
from "../alchemy.run.ts";
const {
const test: TestFn
test
,
const beforeAll: BeforeAllFn
beforeAll
,
const afterAll: AfterAllFn
afterAll
,
const deploy: <A>(stack: Test.TestEffect<CompiledStack<A>, Stage | AlchemyContext>, options?: {
stage?: string;
}) => ReturnType<typeof deploy>
deploy
,
const destroy: (stack: Test.TestEffect<CompiledStack, Stage | AlchemyContext>, options?: {
stage?: string;
}) => ReturnType<typeof destroy>
destroy
} =
import Test
Test
.
const make: <Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry>(options: Test.MakeOptions<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry>) => Test.TestApi

Build the per-file test API. Configure providers / state once at the top of the test file:

import * as Test from "alchemy/Test/Bun";
import * as Cloudflare from "alchemy/Cloudflare";
const { test, deploy, destroy, beforeAll, afterAll } = Test.make({
providers: Cloudflare.providers(),
state: Cloudflare.state(),
});

make
({
MakeOptions<Providers | Provider<Command> | Provider<Random> | Credentials | CloudflareEnvironment | Access | Retry>.providers: Layer<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry, never, StackServices>

Provider layer for the stack — e.g. AWS.providers(), Cloudflare.providers().

providers
:
import Cloudflare
Cloudflare
.
const providers: () => Layer<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry, never, any>

Cloudflare providers, bindings, and credentials for Worker-based stacks.

providers
(),
MakeOptions<ROut = any>.state?: Layer<State, never, StackServices> | undefined

State store for top-level deploy(Stack) / destroy(Stack); defaults to

State.localState

.

state
:
import Cloudflare
Cloudflare
.
function state(props?: {
workerName?: string;
noTrack?: boolean;
}): Layer<...>
export state
state
(),
});

expect (and any other bun:test helpers like describe) come from bun:test directly — Test.make only provides the Effect-aware test runner pieces.

Use beforeAll with deploy to deploy your stack once before any tests run:

test/integ.test.ts
import * as
import Cloudflare
Cloudflare
from "alchemy/Cloudflare";
import * as
import Test
Test
from "alchemy/Test/Bun";
import {
const expect: Expect

Asserts that a value matches some criteria.

@linkhttps://jestjs.io/docs/expect#reference

@example expect(1 + 1).toBe(2); expect([1,2,3]).toContain(2); expect(null).toBeNull();

@paramactual The actual (received) value

expect
} from "bun:test";
import * as
import Effect
Effect
from "effect/Effect";
import
import Stack
Stack
from "../alchemy.run.ts";
const {
const test: TestFn
test
,
const beforeAll: BeforeAllFn
beforeAll
,
const afterAll: AfterAllFn
afterAll
,
const deploy: <A>(stack: Test.TestEffect<CompiledStack<A>, Stage | AlchemyContext>, options?: {
stage?: string;
}) => ReturnType<typeof deploy>
deploy
,
const destroy: (stack: Test.TestEffect<CompiledStack, Stage | AlchemyContext>, options?: {
stage?: string;
}) => ReturnType<typeof destroy>
destroy
} =
import Test
Test
.
const make: <Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry>(options: Test.MakeOptions<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry>) => Test.TestApi

Build the per-file test API. Configure providers / state once at the top of the test file:

import * as Test from "alchemy/Test/Bun";
import * as Cloudflare from "alchemy/Cloudflare";
const { test, deploy, destroy, beforeAll, afterAll } = Test.make({
providers: Cloudflare.providers(),
state: Cloudflare.state(),
});

make
({
MakeOptions<Providers | Provider<Command> | Provider<Random> | Credentials | CloudflareEnvironment | Access | Retry>.providers: Layer<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry, never, StackServices>

Provider layer for the stack — e.g. AWS.providers(), Cloudflare.providers().

providers
:
import Cloudflare
Cloudflare
.
const providers: () => Layer<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry, never, any>

Cloudflare providers, bindings, and credentials for Worker-based stacks.

providers
(),
MakeOptions<ROut = any>.state?: Layer<State, never, StackServices> | undefined

State store for top-level deploy(Stack) / destroy(Stack); defaults to

State.localState

.

state
:
import Cloudflare
Cloudflare
.
function state(props?: {
workerName?: string;
noTrack?: boolean;
}): Layer<...>
export state
state
(),
});
const
const stack: Effect.Effect<never, never, never>
stack
=
const beforeAll: BeforeAllFn
<never>(eff: Test.TestEffect<never, never>, options?: test.HookOptions) => Effect.Effect<never, never, never>
beforeAll
(
const deploy: <unknown>(stack: Test.TestEffect<CompiledStack<unknown, any>, Stage | AlchemyContext>, options?: {
stage?: string;
}) => Effect.Effect<never, Stage | AlchemyContext | PlatformError | InvalidReferenceError | MissingSourceError | StateStoreError, FileSystem | Path | AlchemyContext | Cli | State>
deploy
(
import Stack
Stack
));

deploy(Stack) returns an Effect that plans and applies the stack. beforeAll runs it once, then returns a lazy accessor you can yield* inside each test to get the stack outputs.

Write your first test. Use yield* stack to get the outputs you returned from your Stack in Part 2:

test/integ.test.ts
import * as
import Cloudflare
Cloudflare
from "alchemy/Cloudflare";
import * as
import Test
Test
from "alchemy/Test/Bun";
import {
const expect: Expect

Asserts that a value matches some criteria.

@linkhttps://jestjs.io/docs/expect#reference

@example expect(1 + 1).toBe(2); expect([1,2,3]).toContain(2); expect(null).toBeNull();

@paramactual The actual (received) value

expect
} from "bun:test";
import * as
import Effect
Effect
from "effect/Effect";
import
import Stack
Stack
from "../alchemy.run.ts";
const {
const test: TestFn
test
,
const beforeAll: BeforeAllFn
beforeAll
,
const afterAll: AfterAllFn
afterAll
,
const deploy: <A>(stack: Test.TestEffect<CompiledStack<A>, Stage | AlchemyContext>, options?: {
stage?: string;
}) => ReturnType<typeof deploy>
deploy
,
const destroy: (stack: Test.TestEffect<CompiledStack, Stage | AlchemyContext>, options?: {
stage?: string;
}) => ReturnType<typeof destroy>
destroy
} =
import Test
Test
.
const make: <Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry>(options: Test.MakeOptions<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry>) => Test.TestApi

Build the per-file test API. Configure providers / state once at the top of the test file:

import * as Test from "alchemy/Test/Bun";
import * as Cloudflare from "alchemy/Cloudflare";
const { test, deploy, destroy, beforeAll, afterAll } = Test.make({
providers: Cloudflare.providers(),
state: Cloudflare.state(),
});

make
({
MakeOptions<Providers | Provider<Command> | Provider<Random> | Credentials | CloudflareEnvironment | Access | Retry>.providers: Layer<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry, never, StackServices>

Provider layer for the stack — e.g. AWS.providers(), Cloudflare.providers().

providers
:
import Cloudflare
Cloudflare
.
const providers: () => Layer<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry, never, any>

Cloudflare providers, bindings, and credentials for Worker-based stacks.

providers
(),
MakeOptions<ROut = any>.state?: Layer<State, never, StackServices> | undefined

State store for top-level deploy(Stack) / destroy(Stack); defaults to

State.localState

.

state
:
import Cloudflare
Cloudflare
.
function state(props?: {
workerName?: string;
noTrack?: boolean;
}): Layer<...>
export state
state
(),
});
const
const stack: Effect.Effect<never, never, never>
stack
=
const beforeAll: BeforeAllFn
<never>(eff: Test.TestEffect<never, never>, options?: test.HookOptions) => Effect.Effect<never, never, never>
beforeAll
(
const deploy: <unknown>(stack: Test.TestEffect<CompiledStack<unknown, any>, Stage | AlchemyContext>, options?: {
stage?: string;
}) => Effect.Effect<never, Stage | AlchemyContext | PlatformError | InvalidReferenceError | MissingSourceError | StateStoreError, FileSystem | Path | AlchemyContext | Cli | State>
deploy
(
import Stack
Stack
));
const test: TestFn
(name: string, eff: Test.TestEffect<void>, options?: TestOptions) => void
test
(
"worker returns a url",
import Effect
Effect
.
const gen: <Effect.Effect<never, never, never>, void>(f: () => Generator<Effect.Effect<never, never, never>, void, never>) => Effect.Effect<void, never, never> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

@example

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

@categoryCreating Effects

gen
(function* () {
const {
const url: never
url
} = yield*
const stack: Effect.Effect<never, never, never>
stack
;
function expect(actual?: never, customFailMessage?: string): Matchers<undefined> (+2 overloads)

@paramactual the actual value

@paramcustomFailMessage an optional custom message to display if the test fails.

expect
(
const url: never
url
).
MatchersBuiltin<undefined>.toBeString(): void

Asserts that a value is a string.

@example expect("foo").toBeString(); expect(new String("bar")).toBeString(); expect(123).not.toBeString();

toBeString
();
}),
);

test(name, effect) wraps bun:test — you write an Effect generator instead of an async function.

Terminal window
bun test test/integ.test.ts

The first run deploys the stack (or reuses the existing one if already deployed). Subsequent runs are fast because Alchemy diffs and skips unchanged resources.

The basic test just checks that a URL exists. Let’s verify the Worker actually handles requests:

test/integ.test.ts
import * as
import Cloudflare
Cloudflare
from "alchemy/Cloudflare";
import * as
import Test
Test
from "alchemy/Test/Bun";
import {
const expect: Expect

Asserts that a value matches some criteria.

@linkhttps://jestjs.io/docs/expect#reference

@example expect(1 + 1).toBe(2); expect([1,2,3]).toContain(2); expect(null).toBeNull();

@paramactual The actual (received) value

expect
} from "bun:test";
import * as
import Effect
Effect
from "effect/Effect";
import * as
import HttpBody
HttpBody
from "effect/unstable/http/HttpBody";
import * as
import HttpClient
HttpClient
from "effect/unstable/http/HttpClient";
import
import Stack
Stack
from "../alchemy.run.ts";
const {
const test: TestFn
test
,
const beforeAll: BeforeAllFn
beforeAll
,
const afterAll: AfterAllFn
afterAll
,
const deploy: <A>(stack: Test.TestEffect<CompiledStack<A>, Stage | AlchemyContext>, options?: {
stage?: string;
}) => ReturnType<typeof deploy>
deploy
,
const destroy: (stack: Test.TestEffect<CompiledStack, Stage | AlchemyContext>, options?: {
stage?: string;
}) => ReturnType<typeof destroy>
destroy
} =
import Test
Test
.
const make: <Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry>(options: Test.MakeOptions<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry>) => Test.TestApi

Build the per-file test API. Configure providers / state once at the top of the test file:

import * as Test from "alchemy/Test/Bun";
import * as Cloudflare from "alchemy/Cloudflare";
const { test, deploy, destroy, beforeAll, afterAll } = Test.make({
providers: Cloudflare.providers(),
state: Cloudflare.state(),
});

make
({
MakeOptions<Providers | Provider<Command> | Provider<Random> | Credentials | CloudflareEnvironment | Access | Retry>.providers: Layer<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry, never, StackServices>

Provider layer for the stack — e.g. AWS.providers(), Cloudflare.providers().

providers
:
import Cloudflare
Cloudflare
.
const providers: () => Layer<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry, never, any>

Cloudflare providers, bindings, and credentials for Worker-based stacks.

providers
(),
MakeOptions<ROut = any>.state?: Layer<State, never, StackServices> | undefined

State store for top-level deploy(Stack) / destroy(Stack); defaults to

State.localState

.

state
:
import Cloudflare
Cloudflare
.
function state(props?: {
workerName?: string;
noTrack?: boolean;
}): Layer<...>
export state
state
(),
});
const
const stack: Effect.Effect<never, never, never>
stack
=
const beforeAll: BeforeAllFn
<never>(eff: Test.TestEffect<never, never>, options?: test.HookOptions) => Effect.Effect<never, never, never>
beforeAll
(
const deploy: <unknown>(stack: Test.TestEffect<CompiledStack<unknown, any>, Stage | AlchemyContext>, options?: {
stage?: string;
}) => Effect.Effect<never, Stage | AlchemyContext | PlatformError | InvalidReferenceError | MissingSourceError | StateStoreError, FileSystem | Path | AlchemyContext | Cli | State>
deploy
(
import Stack
Stack
));
const test: TestFn
(name: string, eff: Test.TestEffect<void>, options?: TestOptions) => void
test
(
"worker returns a url",
import Effect
Effect
.
const gen: <Effect.Effect<never, never, never>, void>(f: () => Generator<Effect.Effect<never, never, never>, void, never>) => Effect.Effect<void, never, never> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

@example

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

@categoryCreating Effects

gen
(function* () {
const {
const url: never
url
} = yield*
const stack: Effect.Effect<never, never, never>
stack
;
function expect(actual?: never, customFailMessage?: string): Matchers<undefined> (+2 overloads)

@paramactual the actual value

@paramcustomFailMessage an optional custom message to display if the test fails.

expect
(
const url: never
url
).
MatchersBuiltin<undefined>.toBeString(): void

Asserts that a value is a string.

@example expect("foo").toBeString(); expect(new String("bar")).toBeString(); expect(123).not.toBeString();

toBeString
();
}),
);
const test: TestFn
(name: string, eff: Test.TestEffect<void>, options?: TestOptions) => void
test
(
"PUT and GET round-trip an object",
import Effect
Effect
.
const gen: <Effect.Effect<string, HttpClientError, never> | Effect.Effect<HttpClientResponse, HttpClientError, HttpClient.HttpClient>, void>(f: () => Generator<Effect.Effect<string, HttpClientError, never> | Effect.Effect<HttpClientResponse, HttpClientError, HttpClient.HttpClient>, void, never>) => Effect.Effect<void, HttpClientError, HttpClient.HttpClient> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

@example

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

@categoryCreating Effects

gen
(function* () {
const {
const url: never
url
} = yield*
const stack: Effect.Effect<never, never, never>
stack
;
const
const put: HttpClientResponse
put
= yield*
import HttpClient
HttpClient
.
const put: (url: string | URL, options?: Options.NoUrl | undefined) => Effect.Effect<HttpClientResponse, HttpClientError, HttpClient.HttpClient>

@since4.0.0

@categoryaccessors

put
(`${
const url: never
url
}/hello.txt`, {
body?: HttpBody.HttpBody | undefined
body
:
import HttpBody
HttpBody
.
const text: (body: string, contentType?: string) => HttpBody.Uint8Array

@since4.0.0

@categoryconstructors

text
("Hello, World!"),
});
expect<number>(actual: number, customFailMessage?: string): Matchers<number> (+2 overloads)

@paramactual the actual value

@paramcustomFailMessage an optional custom message to display if the test fails.

expect
(
const put: HttpClientResponse
put
.
HttpClientResponse.status: number
status
).
MatchersBuiltin<number>.toBe(expected: number): void (+1 overload)

Asserts that a value equals what is expected.

  • For non-primitive values, like objects and arrays, use toEqual() instead.
  • For floating-point numbers, use toBeCloseTo() instead.

@example

expect(100 + 23).toBe(123); expect("d" + "og").toBe("dog"); expect([123]).toBe([123]); // fail, use toEqual() expect(3 + 0.14).toBe(3.14); // fail, use toBeCloseTo()

// TypeScript errors: expect("hello").toBe(3.14); // typescript error + fail expect("hello").toBe(3.14); // no typescript error, but still fails

@paramexpected the expected value

toBe
(201);
const
const get: HttpClientResponse
get
= yield*
import HttpClient
HttpClient
.
const get: (url: string | URL, options?: Options.NoUrl | undefined) => Effect.Effect<HttpClientResponse, HttpClientError, HttpClient.HttpClient>

@since4.0.0

@categoryaccessors

get
(`${
const url: never
url
}/hello.txt`);
expect<string>(actual: string, customFailMessage?: string): Matchers<string> (+2 overloads)

@paramactual the actual value

@paramcustomFailMessage an optional custom message to display if the test fails.

expect
(yield*
const get: HttpClientResponse
get
.
HttpIncomingMessage<HttpClientError>.text: Effect.Effect<string, HttpClientError, never>
text
).
MatchersBuiltin<string>.toBe(expected: string): void (+1 overload)

Asserts that a value equals what is expected.

  • For non-primitive values, like objects and arrays, use toEqual() instead.
  • For floating-point numbers, use toBeCloseTo() instead.

@example

expect(100 + 23).toBe(123); expect("d" + "og").toBe("dog"); expect([123]).toBe([123]); // fail, use toEqual() expect(3 + 0.14).toBe(3.14); // fail, use toBeCloseTo()

// TypeScript errors: expect("hello").toBe(3.14); // typescript error + fail expect("hello").toBe(3.14); // no typescript error, but still fails

@paramexpected the expected value

toBe
("Hello, World!");
}),
);
const test: TestFn
(name: string, eff: Test.TestEffect<void>, options?: TestOptions) => void
test
(
"GET missing key returns 404",
import Effect
Effect
.
const gen: <Effect.Effect<HttpClientResponse, HttpClientError, HttpClient.HttpClient>, void>(f: () => Generator<Effect.Effect<HttpClientResponse, HttpClientError, HttpClient.HttpClient>, void, never>) => Effect.Effect<void, HttpClientError, HttpClient.HttpClient> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

@example

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

@categoryCreating Effects

gen
(function* () {
const {
const url: never
url
} = yield*
const stack: Effect.Effect<never, never, never>
stack
;
const
const response: HttpClientResponse
response
= yield*
import HttpClient
HttpClient
.
const get: (url: string | URL, options?: Options.NoUrl | undefined) => Effect.Effect<HttpClientResponse, HttpClientError, HttpClient.HttpClient>

@since4.0.0

@categoryaccessors

get
(`${
const url: never
url
}/no-such-key`);
expect<number>(actual: number, customFailMessage?: string): Matchers<number> (+2 overloads)

@paramactual the actual value

@paramcustomFailMessage an optional custom message to display if the test fails.

expect
(
const response: HttpClientResponse
response
.
HttpClientResponse.status: number
status
).
MatchersBuiltin<number>.toBe(expected: number): void (+1 overload)

Asserts that a value equals what is expected.

  • For non-primitive values, like objects and arrays, use toEqual() instead.
  • For floating-point numbers, use toBeCloseTo() instead.

@example

expect(100 + 23).toBe(123); expect("d" + "og").toBe("dog"); expect([123]).toBe([123]); // fail, use toEqual() expect(3 + 0.14).toBe(3.14); // fail, use toBeCloseTo()

// TypeScript errors: expect("hello").toBe(3.14); // typescript error + fail expect("hello").toBe(3.14); // no typescript error, but still fails

@paramexpected the expected value

toBe
(404);
}),
);

HttpClient is provided automatically by the test harness — no extra setup needed.

Right now the stack stays deployed after tests finish. That’s great locally — you can re-run tests instantly against the already-deployed stack. But on CI you want to clean up.

Add afterAll with destroy, using skipIf to only tear down when CI is set:

import * as
import Cloudflare
Cloudflare
from "alchemy/Cloudflare";
import * as
import Test
Test
from "alchemy/Test/Bun";
import {
const expect: Expect

Asserts that a value matches some criteria.

@linkhttps://jestjs.io/docs/expect#reference

@example expect(1 + 1).toBe(2); expect([1,2,3]).toContain(2); expect(null).toBeNull();

@paramactual The actual (received) value

expect
} from "bun:test";
import * as
import Effect
Effect
from "effect/Effect";
import * as
import HttpBody
HttpBody
from "effect/unstable/http/HttpBody";
import * as
import HttpClient
HttpClient
from "effect/unstable/http/HttpClient";
import
import Stack
Stack
from "../alchemy.run.ts";
const {
const test: TestFn
test
,
const beforeAll: BeforeAllFn
beforeAll
,
const afterAll: AfterAllFn
afterAll
,
const deploy: <A>(stack: Test.TestEffect<CompiledStack<A>, Stage | AlchemyContext>, options?: {
stage?: string;
}) => ReturnType<typeof deploy>
deploy
,
const destroy: (stack: Test.TestEffect<CompiledStack, Stage | AlchemyContext>, options?: {
stage?: string;
}) => ReturnType<typeof destroy>
destroy
} =
import Test
Test
.
const make: <Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry>(options: Test.MakeOptions<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry>) => Test.TestApi

Build the per-file test API. Configure providers / state once at the top of the test file:

import * as Test from "alchemy/Test/Bun";
import * as Cloudflare from "alchemy/Cloudflare";
const { test, deploy, destroy, beforeAll, afterAll } = Test.make({
providers: Cloudflare.providers(),
state: Cloudflare.state(),
});

make
({
MakeOptions<Providers | Provider<Command> | Provider<Random> | Credentials | CloudflareEnvironment | Access | Retry>.providers: Layer<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry, never, StackServices>

Provider layer for the stack — e.g. AWS.providers(), Cloudflare.providers().

providers
:
import Cloudflare
Cloudflare
.
const providers: () => Layer<Cloudflare.Providers | Provider<Command> | Provider<Random> | Cloudflare.Credentials | Cloudflare.CloudflareEnvironment | Access | Retry, never, any>

Cloudflare providers, bindings, and credentials for Worker-based stacks.

providers
(),
MakeOptions<ROut = any>.state?: Layer<State, never, StackServices> | undefined

State store for top-level deploy(Stack) / destroy(Stack); defaults to

State.localState

.

state
:
import Cloudflare
Cloudflare
.
function state(props?: {
workerName?: string;
noTrack?: boolean;
}): Layer<...>
export state
state
(),
});
const
const stack: Effect.Effect<never, never, never>
stack
=
const beforeAll: BeforeAllFn
<never>(eff: Test.TestEffect<never, never>, options?: test.HookOptions) => Effect.Effect<never, never, never>
beforeAll
(
const deploy: <unknown>(stack: Test.TestEffect<CompiledStack<unknown, any>, Stage | AlchemyContext>, options?: {
stage?: string;
}) => Effect.Effect<never, Stage | AlchemyContext | PlatformError | InvalidReferenceError | MissingSourceError | StateStoreError, FileSystem | Path | AlchemyContext | Cli | State>
deploy
(
import Stack
Stack
));
const afterAll: AfterAllFn
afterAll
.
AfterAllFn.skipIf: (predicate: boolean) => (eff: Test.TestEffect<any>, options?: test.HookOptions) => void
skipIf
(!
var process: NodeJS.Process
process
.
NodeJS.Process.env: NodeJS.ProcessEnv

The process.env property returns an object containing the user environment. See environ(7).

An example of this object looks like:

{
TERM: 'xterm-256color',
SHELL: '/usr/local/bin/bash',
USER: 'maciej',
PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin',
PWD: '/Users/maciej',
EDITOR: 'vim',
SHLVL: '1',
HOME: '/Users/maciej',
LOGNAME: 'maciej',
_: '/usr/local/bin/node'
}

It is possible to modify this object, but such modifications will not be reflected outside the Node.js process, or (unless explicitly requested) to other Worker threads. In other words, the following example would not work:

Terminal window
node -e 'process.env.foo = "bar"' &#x26;&#x26; echo $foo

While the following will:

import { env } from 'node:process';
env.foo = 'bar';
console.log(env.foo);

Assigning a property on process.env will implicitly convert the value to a string. This behavior is deprecated. Future versions of Node.js may throw an error when the value is not a string, number, or boolean.

import { env } from 'node:process';
env.test = null;
console.log(env.test);
// => 'null'
env.test = undefined;
console.log(env.test);
// => 'undefined'

Use delete to delete a property from process.env.

import { env } from 'node:process';
env.TEST = 1;
delete env.TEST;
console.log(env.TEST);
// => undefined

On Windows operating systems, environment variables are case-insensitive.

import { env } from 'node:process';
env.TEST = 1;
console.log(env.test);
// => 1

Unless explicitly specified when creating a Worker instance, each Worker thread has its own copy of process.env, based on its parent thread's process.env, or whatever was specified as the env option to the Worker constructor. Changes to process.env will not be visible across Worker threads, and only the main thread can make changes that are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner unlike the main thread.

@sincev0.1.27

env
.
string | undefined
CI
)(
const destroy: (stack: Test.TestEffect<CompiledStack, Stage | AlchemyContext>, options?: {
stage?: string;
}) => ReturnType<typeof destroy>
destroy
(
import Stack
Stack
));
const test: TestFn
(name: string, eff: Test.TestEffect<void>, options?: TestOptions) => void
test
(/* .. */);
const test: TestFn
(name: string, eff: Test.TestEffect<void>, options?: TestOptions) => void
test
(/* .. */);
  • LocallyCI is not set, so skipIf skips the destroy. You iterate fast against the live stack.
  • On CI — set CI=true and the stack is torn down automatically after tests complete.

You now have:

  • Test.make({ providers, state }) to wire your provider Layer and state store into the test runner once per file
  • beforeAll(deploy(Stack)) to deploy once before tests
  • yield* stack to access outputs in each test
  • HTTP assertions using Effect’s HttpClient
  • afterAll.skipIf(!process.env.CI)(destroy(Stack)) for automatic cleanup on CI with fast iteration locally

In Part 4, you’ll run your stack locally with alchemy dev for instant feedback during development.