| name | effect-patterns-testing |
| description | Effect-TS patterns for Testing. Use when working with testing in Effect-TS applications. |
Effect-TS Patterns: Testing
This skill provides 10 curated Effect-TS patterns for testing.
Use this skill when working on tasks related to:
- testing
- Best practices in Effect-TS applications
- Real-world patterns and solutions
🟢 Beginner Patterns
Your First Effect Test
Rule: Use Effect.runPromise in tests to run and assert on Effect results.
Good Example:
import { describe, it, expect } from "vitest"
import { Effect } from "effect"
const add = (a: number, b: number): Effect.Effect<number> =>
Effect.succeed(a + b)
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
const fetchUser = (id: string): Effect.Effect<{ id: string; name: string }> =>
Effect.succeed({ id, name: `User ${id}` })
describe("Basic Effect Tests", () => {
it("should add two numbers", async () => {
const result = await Effect.runPromise(add(2, 3))
expect(result).toBe(5)
})
it("should divide numbers", async () => {
const result = await Effect.runPromise(divide(10, 2))
expect(result).toBe(5)
})
it("should fail on divide by zero", async () => {
await expect(Effect.runPromise(divide(10, 0))).rejects.toThrow(
"Cannot divide by zero"
)
})
it("should fetch a user", async () => {
const user = await Effect.runPromise(fetchUser("123"))
expect(user).toEqual({
id: "123",
name: "User 123",
})
})
})
const calculateDiscount = (price: number, quantity: number) =>
Effect.gen(function* () {
if (price <= 0) {
return yield* Effect.fail(new Error("Invalid price"))
}
const subtotal = price * quantity
const discount = quantity >= 10 ? 0.1 : 0
const total = subtotal * (1 - discount)
return { subtotal, discount, total }
})
describe("Effect.gen Tests", () => {
it("should calculate without discount", async () => {
const result = await Effect.runPromise(calculateDiscount(10, 5))
expect(result.subtotal).toBe(50)
expect(result.discount).toBe(0)
expect(result.total).toBe(50)
})
it("should apply bulk discount", async () => {
const result = await Effect.runPromise(calculateDiscount(10, 10))
expect(result.subtotal).toBe(100)
expect(result.discount).toBe(0.1)
expect(result.total).toBe(90)
})
it("should fail for invalid price", async () => {
await expect(
Effect.runPromise(calculateDiscount(-5, 10))
).rejects.toThrow("Invalid price")
})
})
Rationale:
Test Effect programs by running them with Effect.runPromise and using standard test assertions on the results.
Testing Effect code is straightforward:
- Effects are values - Build them in tests like any other value
- Run to get results - Use
Effect.runPromise to execute
- Assert normally - Standard assertions work on the results
Test Effects with Services
Rule: Provide test implementations of services to make Effect programs testable.
Good Example:
import { describe, it, expect } from "vitest"
import { Effect, Context } from "effect"
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{
readonly findById: (id: string) => Effect.Effect<User | null>
readonly save: (user: User) => Effect.Effect<void>
}
>() {}
interface User {
id: string
name: string
email: string
}
const getUser = (id: string) =>
Effect.gen(function* () {
const repo = yield* UserRepository
const user = yield* repo.findById(id)
if (!user) {
return yield* Effect.fail(new Error(`User ${id} not found`))
}
return user
})
const createUser = (name: string, email: string) =>
Effect.gen(function* () {
const repo = yield* UserRepository
const user: User = {
id: crypto.randomUUID(),
name,
email,
}
yield* repo.save(user)
return user
})
const makeTestUserRepository = (initialUsers: User[] = []) => {
const users = new Map(initialUsers.map(u => [u.id, u]))
return UserRepository.of({
findById: (id) => Effect.succeed(users.get(id) ?? null),
save: (user) => Effect.sync(() => { users.set(user.id, user) }),
})
}
describe("User Service Tests", () => {
it("should find an existing user", async () => {
const testUser: User = {
id: "123",
name: "Alice",
email: "alice@example.com",
}
const testRepo = makeTestUserRepository([testUser])
const result = await Effect.runPromise(
getUser("123").pipe(
Effect.provideService(UserRepository, testRepo)
)
)
expect(result).toEqual(testUser)
})
it("should fail when user not found", async () => {
const testRepo = makeTestUserRepository([])
await expect(
Effect.runPromise(
getUser("999").pipe(
Effect.provideService(UserRepository, testRepo)
)
)
).rejects.toThrow("User 999 not found")
})
it("should create and save a user", async () => {
const savedUsers: User[] = []
const trackingRepo = UserRepository.of({
findById: () => Effect.succeed(null),
save: (user) => Effect.sync(() => { savedUsers.push(user) }),
})
const result = await Effect.runPromise(
createUser("Bob", "bob@example.com").pipe(
Effect.provideService(UserRepository, trackingRepo)
)
)
expect(result.name).toBe("Bob")
expect(result.email).toBe("bob@example.com")
expect(savedUsers).toHaveLength(1)
expect(savedUsers[0].name).toBe("Bob")
})
})
Rationale:
When testing Effects that require services, provide test implementations using Effect.provideService or test layers.
Effect's service pattern makes testing easy:
- Declare dependencies - Effects specify what they need
- Inject test doubles - Provide fake implementations for tests
- No mocking libraries - Just provide different service implementations
- Type-safe - Compiler ensures you provide all dependencies
🟡 Intermediate Patterns
Accessing the Current Time with Clock
Rule: Use the Clock service to get the current time, enabling deterministic testing with TestClock.
Good Example:
This example shows a function that checks if a token is expired. Its logic depends on Clock, making it fully testable.
import { Effect, Clock, Duration } from "effect";
interface Token {
readonly value: string;
readonly expiresAt: number;
}
const isTokenExpired = (
token: Token
): Effect.Effect<boolean, never, Clock.Clock> =>
Clock.currentTimeMillis.pipe(
Effect.map((now) => now > token.expiresAt),
Effect.tap((expired) =>
Clock.currentTimeMillis.pipe(
Effect.flatMap((currentTime) =>
Effect.log(
`Token expired? ${expired} (current time: ${new Date(currentTime).toISOString()})`
)
)
)
)
);
const makeTestClock = (timeMs: number): Clock.Clock => ({
currentTimeMillis: Effect.succeed(timeMs),
currentTimeNanos: Effect.succeed(BigInt(timeMs * 1_000_000)),
sleep: (duration: Duration.Duration) => Effect.succeed(void 0),
unsafeCurrentTimeMillis: () => timeMs,
unsafeCurrentTimeNanos: () => BigInt(timeMs * 1_000_000),
[Clock.ClockTypeId]: Clock.ClockTypeId,
});
const token = { value: "abc", expiresAt: Date.now() + 1000 };
const program = Effect.gen(function* () {
yield* Effect.log("Checking with current time...");
yield* isTokenExpired(token);
yield* Effect.log("\nChecking with past time (1 minute ago)...");
const pastClock = makeTestClock(Date.now() - 60_000);
yield* isTokenExpired(token).pipe(
Effect.provideService(Clock.Clock, pastClock)
);
yield* Effect.log("\nChecking with future time (1 hour ahead)...");
const futureClock = makeTestClock(Date.now() + 3600_000);
yield* isTokenExpired(token).pipe(
Effect.provideService(Clock.Clock, futureClock)
);
});
Effect.runPromise(
program.pipe(Effect.provideService(Clock.Clock, makeTestClock(Date.now())))
);
Anti-Pattern:
Directly calling Date.now() inside your business logic. This creates an impure function that cannot be tested reliably without manipulating the system clock, which is a bad practice.
import { Effect } from "effect";
interface Token {
readonly expiresAt: number;
}
const isTokenExpiredUnsafely = (token: Token): Effect.Effect<boolean> =>
Effect.sync(() => Date.now() > token.expiresAt);
Rationale:
Whenever you need to get the current time within an Effect, do not call Date.now() directly. Instead, depend on the Clock service and use one of its methods, such as Clock.currentTimeMillis.
Directly calling Date.now() makes your code impure and tightly coupled to the system clock. This makes testing difficult and unreliable, as the output of your function will change every time it's run.
The Clock service is Effect's solution to this problem. It's an abstraction for "the current time."
- In production, the default
Live Clock implementation uses the real system time.
- In tests, you can provide the
TestClock layer. This gives you a virtual clock that you can manually control, allowing you to set the time to a specific value or advance it by a specific duration.
This makes any time-dependent logic pure, deterministic, and easy to test with perfect precision.
Write Tests That Adapt to Application Code
Rule: Write tests that adapt to application code.
Good Example:
import { Effect } from "effect";
interface User {
id: number;
name: string;
}
class NotFoundError extends Error {
readonly _tag = "NotFoundError";
constructor(readonly id: number) {
super(`User ${id} not found`);
}
}
interface DatabaseServiceApi {
getUserById: (id: number) => Effect.Effect<User, NotFoundError>;
}
class DatabaseService extends Effect.Service<DatabaseService>()(
"DatabaseService",
{
sync: () => ({
getUserById: (id: number) => {
if (id === 404) {
return Effect.fail(new NotFoundError(id));
}
return Effect.succeed({ id, name: `User ${id}` });
},
}),
}
) {}
class TestDatabaseService extends Effect.Service<TestDatabaseService>()(
"TestDatabaseService",
{
sync: () => ({
getUserById: (id: number) => {
const testUsers = [
{ id: 1, name: "Test User 1" },
{ id: 2, name: "Test User 2" },
{ id: 123, name: "User 123" },
];
const user = testUsers.find((u) => u.id === id);
if (user) {
return Effect.succeed(user);
}
return Effect.fail(new NotFoundError(id));
},
}),
}
) {}
const getUserWithFallback = (id: number) =>
Effect.gen(function* () {
const db = yield* DatabaseService;
return yield* Effect.gen(function* () {
const user = yield* db.getUserById(id);
return user;
}).pipe(
Effect.catchAll((error) =>
Effect.gen(function* () {
if (error instanceof NotFoundError) {
yield* Effect.logInfo(`User ${id} not found, using fallback`);
return { id, name: `Fallback User ${id}` };
}
return yield* Effect.fail(error);
})
)
);
});
const program = Effect.gen(function* () {
yield* Effect.logInfo(
"=== Writing Tests that Adapt to Application Code Demo ==="
);
const db = yield* DatabaseService;
yield* Effect.logInfo("\n1. Looking up existing user 123...");
const user = yield* Effect.gen(function* () {
try {
return yield* db.getUserById(123);
} catch (error) {
yield* Effect.logError(
`Failed to get user: ${error instanceof Error ? error.message : "Unknown error"}`
);
return { id: -1, name: "Error" };
}
});
yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);
yield* Effect.logInfo("\n2. Looking up non-existent user 404...");
const notFoundUser = yield* Effect.gen(function* () {
try {
return yield* db.getUserById(404);
} catch (error) {
if (error instanceof NotFoundError) {
yield* Effect.logInfo(
`✅ Properly handled NotFoundError: ${error.message}`
);
return { id: 404, name: "Not Found" };
}
yield* Effect.logError(
`Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`
);
return { id: -1, name: "Error" };
}
});
yield* Effect.logInfo(`Result: ${JSON.stringify(notFoundUser)}`);
yield* Effect.logInfo("\n3. Business logic with fallback for missing user:");
const userWithFallback = yield* getUserWithFallback(999);
yield* Effect.logInfo(
`User with fallback: ${JSON.stringify(userWithFallback)}`
);
yield* Effect.logInfo("\n4. Testing with test service implementation:");
yield* Effect.provide(
Effect.gen(function* () {
const testDb = yield* TestDatabaseService;
const testUser1 = yield* Effect.gen(function* () {
try {
return yield* testDb.getUserById(1);
} catch (error) {
yield* Effect.logError(
`Test failed: ${error instanceof Error ? error.message : "Unknown error"}`
);
return { id: -1, name: "Test Error" };
}
});
yield* Effect.logInfo(`Test user 1: ${JSON.stringify(testUser1)}`);
const testUser404 = yield* Effect.gen(function* () {
try {
return yield* testDb.getUserById(404);
} catch (error) {
yield* Effect.logInfo(
`✅ Test service properly threw NotFoundError: ${error instanceof Error ? error.message : "Unknown error"}`
);
return { id: 404, name: "Test Not Found" };
}
});
yield* Effect.logInfo(`Test result: ${JSON.stringify(testUser404)}`);
}),
TestDatabaseService.Default
);
yield* Effect.logInfo(
"\n✅ Tests that adapt to application code demonstration completed!"
);
yield* Effect.logInfo(
"The same business logic works with different service implementations!"
);
});
Effect.runPromise(
Effect.provide(program, DatabaseService.Default) as Effect.Effect<
void,
never,
never
>
);
Explanation:
Tests should reflect the real interface and behavior of your code, not force changes to it.
Anti-Pattern:
Any action where the test dictates a change to the application code. Do not modify a service file to add a method just because a test needs it. If a test fails, fix the test.
Rationale:
Tests are secondary artifacts that serve to validate the application. The application's code and interfaces are the source of truth. When a test fails, fix the test's logic or setup, not the production code.
Treating application code as immutable during testing prevents the introduction of bugs and false test confidence. The goal of a test is to verify real-world behavior; changing that behavior to suit the test invalidates its purpose.
Use the Auto-Generated .Default Layer in Tests
Rule: Use the auto-generated .Default layer in tests.
Good Example:
import { Effect } from "effect";
class MyService extends Effect.Service<MyService>()("MyService", {
sync: () => ({
doSomething: () =>
Effect.succeed("done").pipe(
Effect.tap(() => Effect.log("MyService did something!"))
),
}),
}) {}
const program = Effect.gen(function* () {
yield* Effect.log("Getting MyService...");
const service = yield* MyService;
yield* Effect.log("Calling doSomething()...");
const result = yield* service.doSomething();
yield* Effect.log(`Result: ${result}`);
});
Effect.runPromise(Effect.provide(program, MyService.Default));
Explanation:
This approach ensures your tests are idiomatic, maintainable, and take full advantage of Effect's dependency injection system.
Anti-Pattern:
Do not create manual layers for your service in tests (Layer.succeed(...)) or try to provide the service class directly. This bypasses the intended dependency injection mechanism.
Rationale:
In your tests, provide service dependencies using the static .Default property that Effect.Service automatically attaches to your service class.
The .Default layer is the canonical way to provide a service in a test environment. It's automatically created, correctly scoped, and handles resolving any transitive dependencies, making tests cleaner and more robust.
Mocking Dependencies in Tests
Rule: Provide mock service implementations via a test-specific Layer to isolate the unit under test.
Good Example:
We want to test a Notifier service that uses an EmailClient to send emails. In our test, we provide a mock EmailClient that doesn't actually send emails but just returns a success value.
import { Effect, Layer } from "effect";
interface EmailClientService {
send: (address: string, body: string) => Effect.Effect<void>;
}
class EmailClient extends Effect.Service<EmailClientService>()("EmailClient", {
sync: () => ({
send: (address: string, body: string) =>
Effect.sync(() => Effect.log(`Sending email to ${address}: ${body}`)),
}),
}) {}
interface NotifierService {
notifyUser: (userId: number, message: string) => Effect.Effect<void>;
}
class Notifier extends Effect.Service<NotifierService>()("Notifier", {
effect: Effect.gen(function* () {
const emailClient = yield* EmailClient;
return {
notifyUser: (userId: number, message: string) =>
emailClient.send(`user-${userId}@example.com`, message),
};
}),
dependencies: [EmailClient.Default],
}) {}
const program = Effect.gen(function* () {
yield* Effect.log("Using default EmailClient implementation...");
const notifier = yield* Notifier;
yield* notifier.notifyUser(123, "Your invoice is ready.");
yield* Effect.log("\nUsing mock EmailClient implementation...");
const mockEmailClient = Layer.succeed(EmailClient, {
send: (address: string, body: string) =>
Effect.log(`MOCK: Would send to ${address} with body: ${body}`),
} as EmailClientService);
yield* Effect.gen(function* () {
const notifier = yield* Notifier;
yield* notifier.notifyUser(123, "Your invoice is ready.");
}).pipe(Effect.provide(mockEmailClient));
});
Effect.runPromise(Effect.provide(program, Notifier.Default));
Anti-Pattern:
Testing your business logic using the "live" implementation of its dependencies. This creates an integration test, not a unit test. It will be slow, unreliable, and may have real-world side effects (like actually sending an email).
import { Effect } from "effect";
import { NotifierLive } from "./somewhere";
import { EmailClientLive } from "./somewhere";
it("sends a real email", () =>
Effect.gen(function* () {
const notifier = yield* Notifier;
yield* notifier.notifyUser(123, "This is a test email!");
}).pipe(
Effect.provide(NotifierLive),
Effect.provide(EmailClientLive),
Effect.runPromise
));
Rationale:
To test a piece of code in isolation, identify its service dependencies and provide mock implementations for them using a test-specific Layer. The most common way to create a mock layer is with Layer.succeed(ServiceTag, mockImplementation).
The primary goal of a unit test is to verify the logic of a single unit of code, independent of its external dependencies. Effect's dependency injection system is designed to make this easy and type-safe.
By providing a mock Layer in your test, you replace a real dependency (like an HttpClient that makes network calls) with a fake one that returns predictable data. This provides several key benefits:
- Determinism: Your tests always produce the same result, free from the flakiness of network or database connections.
- Speed: Tests run instantly without waiting for slow I/O operations.
- Type Safety: The TypeScript compiler ensures your mock implementation perfectly matches the real service's interface, preventing your tests from becoming outdated.
- Explicitness: The test setup clearly documents all the dependencies required for the code to run.
🟠 Advanced Patterns
Organize Layers into Composable Modules
Rule: Organize services into modular Layers that are composed hierarchically to manage complexity in large applications.
Good Example:
This example shows a BaseLayer with a Logger, a UserModule that uses the Logger, and a final AppLayer that wires them together.
1. The Base Infrastructure Layer
import { Effect } from "effect";
export class Logger extends Effect.Service<Logger>()("App/Core/Logger", {
sync: () => ({
log: (msg: string) => Effect.log(`[LOG] ${msg}`),
}),
}) {}
export class UserRepository extends Effect.Service<UserRepository>()(
"App/User/UserRepository",
{
effect: Effect.gen(function* () {
const logger = yield* Logger;
return {
findById: (id: number) =>
Effect.gen(function* () {
yield* logger.log(`Finding user ${id}`);
return { id, name: `User ${id}` };
}),
};
}),
dependencies: [Logger.Default],
}
) {}
const program = Effect.gen(function* () {
const repo = yield* UserRepository;
const user = yield* repo.findById(1);
return user;
});
Effect.runPromise(Effect.provide(program, UserRepository.Default));
const programWithLogging = Effect.gen(function* () {
const result = yield* program;
yield* Effect.log(`Program result: ${JSON.stringify(result)}`);
return result;
});
Effect.runPromise(Effect.provide(programWithLogging, UserRepository.Default));
2. The Feature Module Layer
import { Effect } from "effect";
export class Logger extends Effect.Service<Logger>()("App/Core/Logger", {
sync: () => ({
log: (msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`)),
}),
}) {}
export class UserRepository extends Effect.Service<UserRepository>()(
"App/User/UserRepository",
{
effect: Effect.gen(function* () {
const logger = yield* Logger;
return {
findById: (id: number) =>
Effect.gen(function* () {
yield* logger.log(`Finding user ${id}`);
return { id, name: `User ${id}` };
}),
};
}),
dependencies: [Logger.Default],
}
) {}
const program = Effect.gen(function* () {
const repo = yield* UserRepository;
const user = yield* repo.findById(1);
return user;
});
Effect.runPromise(Effect.provide(program, UserRepository.Default)).then(
console.log
);
3. The Final Application Composition
import { Layer } from "effect";
import { BaseLayer } from "./core";
import { UserModuleLive } from "./features/User";
const AllModules = Layer.mergeAll(UserModuleLive );
export const AppLayer = Layer.provide(AllModules, BaseLayer);
Anti-Pattern:
A flat composition strategy for a large application. While simple at first, it quickly becomes difficult to manage.
const AppLayer = Layer.mergeAll(
LoggerLive,
ConfigLive,
DatabaseLive,
TracerLive,
UserServiceLive,
UserRepositoryLive,
ProductServiceLive,
ProductRepositoryLive,
BillingServiceLive
);
Rationale:
For large applications, avoid a single, flat list of services. Instead, structure your application by creating hierarchical layers:
BaseLayer: Provides application-wide infrastructure (Logger, Config, Database).
FeatureModule Layers: Provide the services for a specific business domain (e.g., UserModule, ProductModule). These depend on the BaseLayer.
AppLayer: The top-level layer that composes the feature modules by providing them with the BaseLayer.
As an application grows, a flat composition strategy where all services are merged into one giant layer becomes unwieldy and hard to reason about. The Composable Modules pattern solves this by introducing structure.
This approach creates a clean, scalable, and highly testable architecture where complexity is contained within each module. The top-level composition becomes a clear, high-level diagram of your application's architecture, and feature modules can be tested in isolation by providing them with a mocked BaseLayer.
Test Streaming Effects
Rule: Use Stream.runCollect and assertions to verify stream behavior.
Good Example:
import { describe, it, expect } from "vitest"
import { Effect, Stream, Chunk, Ref } from "effect"
describe("Stream Testing", () => {
it("should transform stream elements", async () => {
const result = await Effect.runPromise(
Stream.fromIterable([1, 2, 3, 4, 5]).pipe(
Stream.map((n) => n * 2),
Stream.runCollect
)
)
expect(Chunk.toReadonlyArray(result)).toEqual([2, 4, 6, 8, 10])
})
it("should filter stream elements", async () => {
const result = await Effect.runPromise(
Stream.fromIterable([1, 2, 3, 4, 5, 6]).pipe(
Stream.filter((n) => n % 2 === 0),
Stream.runCollect
)
)
expect(Chunk.toReadonlyArray(result)).toEqual([2, 4, 6])
})
it("should fold stream to single value", async () => {
const result = await Effect.runPromise(
Stream.fromIterable([1, 2, 3, 4, 5]).pipe(
Stream.runFold(0, (acc, n) => acc + n)
)
)
expect(result).toBe(15)
})
it("should count stream elements", async () => {
const count = await Effect.runPromise(
Stream.fromIterable(["a", "b", "c", "d"]).pipe(
Stream.runCount
)
)
expect(count).toBe(4)
})
it("should catch errors in stream", async () => {
const result = await Effect.runPromise(
Stream.fromIterable([1, 2, 3]).pipe(
Stream.mapEffect((n) =>
n === 2
? Effect.fail(new Error("Failed on 2"))
: Effect.succeed(n * 10)
),
Stream.catchAll((error) =>
Stream.succeed(-1)
),
Stream.runCollect
)
)
expect(Chunk.toReadonlyArray(result)).toEqual([10, -1])
})
it("should handle errors and continue with orElse", async () => {
const failingStream = Stream.fail(new Error("Primary failed"))
const fallbackStream = Stream.fromIterable([1, 2, 3])
const result = await Effect.runPromise(
failingStream.pipe(
Stream.orElse(() => fallbackStream),
Stream.runCollect
)
)
expect(Chunk.toReadonlyArray(result)).toEqual([1, 2, 3])
})
it("should chunk stream elements", async () => {
const result = await Effect.runPromise(
Stream.fromIterable([1, 2, 3, 4, 5]).pipe(
Stream.grouped(2),
Stream.runCollect
)
)
const chunks = Chunk.toReadonlyArray(result).map(Chunk.toReadonlyArray)
expect(chunks).toEqual([[1, 2], [3, 4], [5]])
})
it("should run effects for each element", async () => {
const processed: number[] = []
await Effect.runPromise(
Stream.fromIterable([1, 2, 3]).pipe(
Stream.tap((n) =>
Effect.sync(() => {
processed.push(n)
})
),
Stream.runDrain
)
)
expect(processed).toEqual([1, 2, 3])
})
it("should release resources on completion", async () => {
const acquired: string[] = []
const released: string[] = []
const managedStream = Stream.acquireRelease(
Effect.gen(function* () {
acquired.push("resource")
return "resource"
}),
() =>
Effect.sync(() => {
released.push("resource")
})
).pipe(
Stream.flatMap(() => Stream.fromIterable([1, 2, 3]))
)
await Effect.runPromise(Stream.runDrain(managedStream))
expect(acquired).toEqual(["resource"])
expect(released).toEqual(["resource"])
})
it("should release resources on error", async () => {
const released: string[] = []
const managedStream = Stream.acquireRelease(
Effect.succeed("resource"),
() => Effect.sync(() => { released.push("released") })
).pipe(
Stream.flatMap(() =>
Stream.fromEffect(Effect.fail(new Error("Oops")))
)
)
await Effect.runPromise(
Stream.runDrain(managedStream).pipe(
Effect.catchAll(() => Effect.void)
)
)
expect(released).toEqual(["released"])
})
it("should take first N elements", async () => {
const result = await Effect.runPromise(
Stream.fromIterable([1, 2, 3, 4, 5]).pipe(
Stream.take(3),
Stream.runCollect
)
)
expect(Chunk.toReadonlyArray(result)).toEqual([1, 2, 3])
})
it("should drop first N elements", async () => {
const result = await Effect.runPromise(
Stream.fromIterable([1, 2, 3, 4, 5]).pipe(
Stream.drop(2),
Stream.runCollect
)
)
expect(Chunk.toReadonlyArray(result)).toEqual([3, 4, 5])
})
it("should merge streams", async () => {
const stream1 = Stream.fromIterable([1, 3, 5])
const stream2 = Stream.fromIterable([2, 4, 6])
const result = await Effect.runPromise(
Stream.merge(stream1, stream2).pipe(
Stream.runCollect
)
)
const array = Chunk.toReadonlyArray(result)
expect(array).toHaveLength(6)
expect(array).toContain(1)
expect(array).toContain(6)
})
})
Rationale:
Test streams by collecting results and verifying transformations, error handling, and resource management.
Stream tests verify:
- Transformations - map, filter, flatMap work correctly
- Error handling - Failures are caught and handled
- Resource safety - Resources are released
- Backpressure - Data flow is controlled
Test Concurrent Code
Rule: Use TestClock and controlled concurrency to make concurrent tests deterministic.
Good Example:
import { describe, it, expect } from "vitest"
import { Effect, Fiber, Ref, TestClock, Duration, Deferred } from "effect"
describe("Concurrent Code Testing", () => {
it("should run effects in parallel", async () => {
const executionOrder: string[] = []
const task1 = Effect.gen(function* () {
yield* Effect.sleep("100 millis")
executionOrder.push("task1")
return 1
})
const task2 = Effect.gen(function* () {
yield* Effect.sleep("50 millis")
executionOrder.push("task2")
return 2
})
const program = Effect.all([task1, task2], { concurrency: 2 })
const result = await Effect.runPromise(
Effect.gen(function* () {
const fiber = yield* Effect.fork(program)
yield* TestClock.adjust("100 millis")
return yield* Fiber.join(fiber)
}).pipe(Effect.provide(TestClock.live))
)
expect(result).toEqual([1, 2])
expect(executionOrder).toContain("task1")
expect(executionOrder).toContain("task2")
})
it("should handle race condition correctly", async () => {
const counter = await Effect.runPromise(
Effect.gen(function* () {
const ref = yield* Ref.make(0)
const increment = Ref.update(ref, (n) => n + 1)
yield* Effect.all(
Array.from({ length: 100 }, () => increment),
{ concurrency: "unbounded" }
)
return yield* Ref.get(ref)
})
)
expect(counter).toBe(100)
})
it("should test fiber lifecycle", async () => {
const events: string[] = []
const program = Effect.gen(function* () {
const fiber = yield* Effect.fork(
Effect.gen(function* () {
events.push("started")
yield* Effect.sleep("1 second")
events.push("completed")
return "result"
})
)
events.push("forked")
yield* Fiber.interrupt(fiber)
events.push("interrupted")
const exit = yield* Fiber.await(fiber)
return exit
})
await Effect.runPromise(program)
expect(events).toEqual(["forked", "started", "interrupted"])
expect(events).not.toContain("completed")
})
it("should timeout slow operations", async () => {
const slowOperation = Effect.gen(function* () {
yield* Effect.sleep("10 seconds")
return "completed"
})
const result = await Effect.runPromise(
Effect.gen(function* () {
const fiber = yield* Effect.fork(
slowOperation.pipe(Effect.timeout("1 second"))
)
yield* TestClock.adjust("2 seconds")
return yield* Fiber.join(fiber)
}).pipe(Effect.provide(TestClock.live))
)
expect(result._tag).toBe("None")
})
it("should synchronize fibers correctly", async () => {
const result = await Effect.runPromise(
Effect.gen(function* () {
const deferred = yield* Deferred.make<string>()
const results: string[] = []
const consumer = Effect.fork(
Effect.gen(function* () {
const value = yield* Deferred.await(deferred)
results.push(`consumed: ${value}`)
})
)
const producer = Effect.gen(function* () {
results.push("producing")
yield* Deferred.succeed(deferred, "data")
results.push("produced")
})
yield* consumer
yield* producer
yield* Effect.sleep("10 millis")
return results
})
)
expect(result).toContain("producing")
expect(result).toContain("produced")
expect(result).toContain("consumed: data")
})
it("should not deadlock with proper resource ordering", async () => {
const result = await Effect.runPromise(
Effect.gen(function* () {
const ref1 = yield* Ref.make(0)
const ref2 = yield* Ref.make(0)
const fiber1 = yield* Effect.fork(
Effect.gen(function* () {
yield* Ref.update(ref1, (n) => n + 1)
yield* Ref.update(ref2, (n) => n + 1)
})
)
const fiber2 = yield* Effect.fork(
Effect.gen(function* () {
yield* Ref.update(ref1, (n) => n + 1)
yield* Ref.update(ref2, (n) => n + 1)
})
)
yield* Fiber.join(fiber1)
yield* Fiber.join(fiber2)
return [yield* Ref.get(ref1), yield* Ref.get(ref2)]
}).pipe(Effect.timeout("1 second"))
)
expect(result._tag).toBe("Some")
expect(result.value).toEqual([2, 2])
})
})
Rationale:
Use Effect's TestClock and fiber control to make concurrent tests deterministic and repeatable.
Concurrent code is hard to test:
- Non-determinism - Different runs, different results
- Race conditions - Timing-dependent bugs
- Deadlocks - Hard to reproduce
- Flaky tests - Pass sometimes, fail others
Effect's test utilities provide control over timing and concurrency.
Property-Based Testing with Effect
Rule: Use property-based testing to find edge cases your example-based tests miss.
Good Example:
import { describe, it, expect } from "vitest"
import { Effect, Option, Either, Schema } from "effect"
import * as fc from "fast-check"
describe("Property-Based Testing with Effect", () => {
it("should satisfy array reverse properties", () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const reversed = arr.slice().reverse()
const doubleReversed = reversed.slice().reverse()
return JSON.stringify(arr) === JSON.stringify(doubleReversed)
})
)
})
it("should satisfy sort idempotence", () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const sorted = arr.slice().sort((a, b) => a - b)
const sortedTwice = sorted.slice().sort((a, b) => a - b)
return JSON.stringify(sorted) === JSON.stringify(sortedTwice)
})
)
})
it("should map then flatMap equals flatMap with mapping", async () => {
await fc.assert(
fc.asyncProperty(fc.integer(), async (n) => {
const f = (x: number) => x * 2
const g = (x: number) => Effect.succeed(x + 1)
const result1 = await Effect.runPromise(
Effect.succeed(n).pipe(
Effect.map(f),
Effect.flatMap(g)
)
)
const result2 = await Effect.runPromise(
Effect.succeed(n).pipe(
Effect.flatMap((x) => g(f(x)))
)
)
return result1 === result2
})
)
})
it("should satisfy Option map identity", () => {
fc.assert(
fc.property(fc.option(fc.integer(), { nil: undefined }), (maybeN) => {
const option = maybeN === undefined ? Option.none() : Option.some(maybeN)
const mapped = Option.map(option, (x) => x)
return Option.getOrElse(option, () => -1) ===
Option.getOrElse(mapped, () => -1)
})
)
})
it("should roundtrip through Schema", async () => {
const UserSchema = Schema.Struct({
name: Schema.String,
age: Schema.Number.pipe(Schema.int(), Schema.positive()),
})
const userArbitrary = fc.record({
name: fc.string({ minLength: 1 }),
age: fc.integer({ min: 1, max: 120 }),
})
await fc.assert(
fc.asyncProperty(userArbitrary, async (user) => {
const encode = Schema.encode(UserSchema)
const decode = Schema.decode(UserSchema)
const encoded = await Effect.runPromise(encode(user))
const decoded = await Effect.runPromise(decode(encoded))
return decoded.name === user.name && decoded.age === user.age
})
)
})
it("should recover from any error", async () => {
await fc.assert(
fc.asyncProperty(
fc.string(),
fc.string(),
async (errorMsg, fallback) => {
const failing = Effect.fail(new Error(errorMsg))
const result = await Effect.runPromise(
failing.pipe(
Effect.catchAll(() => Effect.succeed(fallback))
)
)
return result === fallback
}
)
)
})
interface Email {
readonly _tag: "Email"
readonly value: string
}
const emailArbitrary = fc.emailAddress().map((value): Email => ({
_tag: "Email",
value,
}))
interface UserId {
readonly _tag: "UserId"
readonly value: string
}
const userIdArbitrary = fc.uuid().map((value): UserId => ({
_tag: "UserId",
value,
}))
it("should handle domain types correctly", () => {
fc.assert(
fc.property(emailArbitrary, userIdArbitrary, (email, userId) => {
return email.value.includes("@") && userId.value.length > 0
})
)
})
it("should satisfy monoid properties for string concat", () => {
const empty = ""
const concat = (a: string, b: string) => a + b
fc.assert(
fc.property(fc.string(), fc.string(), fc.string(), (a, b, c) => {
const leftIdentity = concat(empty, a) === a
const rightIdentity = concat(a, empty) === a
const associative = concat(concat(a, b), c) === concat(a, concat(b, c))
return leftIdentity && rightIdentity && associative
})
)
})
it("should handle positive numbers", () => {
fc.assert(
fc.property(
fc.integer({ min: 1, max: 1000000 }),
fc.integer({ min: 1, max: 1000000 }),
(a, b) => {
const result = a / b
return result > 0
}
)
)
})
})
Rationale:
Use property-based testing with fast-check to test invariants and find edge cases automatically.
Property-based testing finds bugs that example tests miss:
- Edge cases - Empty arrays, negative numbers, unicode
- Invariants - Properties that should always hold
- Shrinking - Minimal failing examples
- Coverage - Many inputs from one test