# Effect TS Testing

> This skill should be used when the user asks about "Effect testing", "@effect/vitest", "it.effect", "it.live", "it.scoped", "it.layer", "it.prop", "Schema Arbitrary", "property-based testing", "fast-check", "TestClock", "testing effects", "mocking services", "test layers", "TestContext", "Effect.provide test", "time testing", "Effect test utilities", "unit testing Effect", "generating test data", "flakyTest", "test coverage", "100% coverage", "service testing", "test doubles", "mock services", or needs to understand how to test Effect-based code.

- Skill: `marzun9620/effect-ts-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add marzun9620/effect-ts-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/marzun9620/effect-ts-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: marzun9620 (https://skillmd.com/u/marzun9620)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/marzun9620/effect-ts-testing

---


# Testing in Effect

## Overview

Effect testing uses **`@effect/vitest`** as the standard test runner integration. This package provides Effect-aware test functions that handle Effect execution, scoped resources, layer composition, and TestClock injection automatically.

**The two pillars of Effect testing that enable 100% test coverage:**

1. **Service-Oriented Architecture** — Every external/effectful dependency (API calls, databases, file systems, third-party services, clocks, random number generators) MUST be wrapped in an Effect Service using `Context.Tag`. Tests provide test implementations via Layers, giving you complete control over all I/O and side effects.

2. **Schema-Driven Property Testing** — Since every data type has a Schema, every data type can generate test data via `Arbitrary`. This makes property-based testing the primary approach for verifying domain logic across thousands of automatically generated inputs.

Together, these two pillars mean: **services eliminate external dependencies from tests, and Arbitrary eliminates hand-crafted test data.** The result is fast, deterministic, comprehensive tests with 100% coverage.

**Core testing tools:**

- **@effect/vitest** - Effect-native test runner (`it.effect`, `it.scoped`, `it.live`, `it.layer`, `it.prop`)
- **Effect Services + Test Layers** - Replace ALL external dependencies with test doubles via `Context.Tag` and `it.layer`
- **Schema.Arbitrary** - Generate test data from any Schema (primary approach — never hand-craft test data)
- **Property Testing** - Test invariants with generated data via `it.prop` or fast-check
- **TestClock** - Control time in tests (automatically provided by `it.effect`)

## The Service-Oriented Testing Pattern (CRITICAL)

**This is the most important testing pattern in Effect.** Every external or effectful operation MUST be wrapped in a Service so that tests can provide a test implementation. This is how you achieve 100% test coverage without hitting real APIs, databases, or file systems.

### The Rule

> **If it makes a network call, reads from disk, talks to a database, calls a third-party API, generates random values, or performs any I/O — it MUST be behind an Effect Service.**

### Why Services Are Required for Testing

Without services, your code is **untestable** because it directly depends on external systems:

```typescript
// ❌ UNTESTABLE: Direct API call baked into business logic
const getUser = (id: string) =>
  Effect.tryPromise({
    try: () => fetch(`/api/users/${id}`).then((r) => r.json()),
    catch: (error) => new NetworkError({ cause: error }),
  });

// ❌ UNTESTABLE: Direct database access
const saveOrder = (order: Order) =>
  Effect.tryPromise({
    try: () => db.query("INSERT INTO orders ...", order),
    catch: (error) => new DatabaseError({ cause: error }),
  });
```

With services, your business logic is **pure and fully testable**:

```typescript
// ✅ TESTABLE: Service abstraction for API calls
class UserApi extends Context.Tag("UserApi")<
  UserApi,
  {
    readonly getUser: (id: string) => Effect.Effect<User, UserNotFound | NetworkError>;
    readonly saveUser: (user: User) => Effect.Effect<void, NetworkError>;
  }
>() {}

// ✅ TESTABLE: Service abstraction for database
class OrderRepository extends Context.Tag("OrderRepository")<
  OrderRepository,
  {
    readonly save: (order: Order) => Effect.Effect<void, DatabaseError>;
    readonly findById: (id: string) => Effect.Effect<Order, OrderNotFound>;
  }
>() {}

// ✅ Business logic is pure — depends only on service interfaces
const processOrder = (orderId: string) =>
  Effect.gen(function* () {
    const userApi = yield* UserApi;
    const orderRepo = yield* OrderRepository;

    const order = yield* orderRepo.findById(orderId);
    const user = yield* userApi.getUser(order.userId);
    // ... pure business logic using service abstractions
  });
```

### What MUST Be a Service

Every one of these MUST be wrapped in a `Context.Tag` service:

| External Dependency      | Service Example                                    |
| ------------------------ | -------------------------------------------------- |
| REST/GraphQL API calls   | `UserApi`, `PaymentGateway`, `NotificationService` |
| Database operations      | `UserRepository`, `OrderRepository`                |
| File system access       | `FileStorage`, `ConfigReader`                      |
| Third-party SDKs         | `StripeClient`, `SendGridClient`, `AwsS3Client`    |
| Email/SMS sending        | `EmailService`, `SmsService`                       |
| Message queues           | `EventPublisher`, `QueueConsumer`                  |
| Caching systems          | `CacheService`, `RedisClient`                      |
| Authentication providers | `AuthProvider`, `TokenService`                     |
| External clock/time      | Use Effect's built-in `Clock` service              |
| Random values            | Use Effect's built-in `Random` service             |

### Complete Service + Test Layer Pattern

```typescript
import { Context, Effect, Layer, Schema, Arbitrary } from "effect";
import { it, expect, layer } from "@effect/vitest";
import * as fc from "fast-check";

// 1. Define schemas for domain types
const TransactionId = Schema.String.pipe(
  Schema.pattern(/^txn_[a-zA-Z0-9]{16}$/),
  Schema.annotations({
    arbitrary: () => (fc) => fc.stringMatching(/^txn_[a-zA-Z0-9]{16}$/),
  }),
);

const PaymentStatus = Schema.Literal("succeeded", "pending", "failed");

class PaymentResult extends Schema.Class<PaymentResult>("PaymentResult")({
  transactionId: TransactionId,
  amount: Schema.Number.pipe(Schema.positive()),
  currency: Schema.Literal("usd", "eur", "gbp"),
  status: PaymentStatus,
}) {}

// 2. Define the service interface
class PaymentGateway extends Context.Tag("PaymentGateway")<
  PaymentGateway,
  {
    readonly charge: (amount: number, currency: string) => Effect.Effect<PaymentResult, PaymentError>;
    readonly refund: (transactionId: string) => Effect.Effect<void, RefundError>;
  }
>() {}

// 3. Live implementation (used in production)
const PaymentGatewayLive = Layer.succeed(PaymentGateway, {
  charge: (amount, currency) =>
    Effect.tryPromise({
      try: () => stripe.charges.create({ amount, currency }),
      catch: (error) => new PaymentError({ cause: error }),
    }),
  refund: (transactionId) =>
    Effect.tryPromise({
      try: () => stripe.refunds.create({ charge: transactionId }),
      catch: (error) => new RefundError({ cause: error }),
    }),
});

// 4. Test implementation using Arbitrary — generates varied test data
const PaymentGatewayTest = Layer.effect(
  PaymentGateway,
  Effect.sync(() => ({
    charge: (amount, currency) =>
      Effect.succeed(
        new PaymentResult({
          transactionId: fc.sample(Arbitrary.make(TransactionId)(fc), 1)[0],
          amount,
          currency: currency as "usd" | "eur" | "gbp",
          status: fc.sample(Arbitrary.make(PaymentStatus)(fc), 1)[0],
        }),
      ),
    refund: (_transactionId) => Effect.void,
  })),
);

// 5. Property test with the test layer — 100% coverage, zero external calls
layer(PaymentGatewayTest)("PaymentService", (it) => {
  it.effect.prop("should process payment for any valid amount", [Schema.Number.pipe(Schema.positive())], ([amount]) =>
    Effect.gen(function* () {
      const gateway = yield* PaymentGateway;
      const result = yield* gateway.charge(amount, "usd");
      expect(result.amount).toBe(amount);
      expect(["succeeded", "pending", "failed"]).toContain(result.status);
    }),
  );

  it.effect.prop("should handle refund for any transaction", [TransactionId], ([txnId]) =>
    Effect.gen(function* () {
      const gateway = yield* PaymentGateway;
      yield* gateway.refund(txnId);
      // No error = success
    }),
  );
});
```

### Stateful Test Layers (for Repository Testing)

For services that need to maintain state across operations within a test, use `Layer.effect` with `Ref`:

```typescript
import { Effect, Layer, Ref, Option } from "effect";

const OrderRepositoryTest = Layer.effect(
  OrderRepository,
  Effect.gen(function* () {
    const store = yield* Ref.make<Map<string, Order>>(new Map());

    return {
      save: (order: Order) => Ref.update(store, (m) => new Map(m).set(order.id, order)),

      findById: (id: string) =>
        Effect.gen(function* () {
          const orders = yield* Ref.get(store);
          return yield* Option.match(Option.fromNullable(orders.get(id)), {
            onNone: () => Effect.fail(new OrderNotFound({ orderId: id })),
            onSome: Effect.succeed,
          }).pipe(Effect.flatten);
        }),

      findAll: () => Ref.get(store).pipe(Effect.map((m) => Array.from(m.values()))),
    };
  }),
);
```

### Composing Multiple Test Layers

Real tests often need multiple services. Compose test layers with `Layer.merge` and use Arbitrary for all test data:

```typescript
import { Schema, Arbitrary, Effect, Layer } from "effect";
import { it, expect, layer } from "@effect/vitest";
import * as fc from "fast-check";

// Define schemas for all domain types
const UserId = Schema.String.pipe(Schema.minLength(1));
const OrderId = Schema.String.pipe(Schema.minLength(1));

class OrderItem extends Schema.Class<OrderItem>("OrderItem")({
  productId: Schema.String,
  price: Schema.Number.pipe(Schema.positive()),
  quantity: Schema.Number.pipe(Schema.int(), Schema.positive()),
}) {}

class Order extends Schema.Class<Order>("Order")({
  id: OrderId,
  userId: UserId,
  items: Schema.NonEmptyArray(OrderItem),
  total: Schema.Number.pipe(Schema.positive()),
}) {}

// Compose all test layers
const TestEnv = Layer.merge(UserApiTest, Layer.merge(OrderRepositoryTest, PaymentGatewayTest));

layer(TestEnv)("Order Processing", (it) => {
  it.effect.prop(
    "should process complete order flow for any user and order",
    [Arbitrary.make(UserId), Arbitrary.make(Order)],
    ([userId, order]) =>
      Effect.gen(function* () {
        const userApi = yield* UserApi;
        const orderRepo = yield* OrderRepository;
        const gateway = yield* PaymentGateway;

        // Full integration test with ALL services mocked + generated data
        const user = yield* userApi.getUser(userId);
        yield* orderRepo.save(order);
        const savedOrder = yield* orderRepo.findById(order.id);
        const payment = yield* gateway.charge(savedOrder.total, "usd");

        expect(payment.amount).toBe(savedOrder.total);
      }),
  );
});
```

### Anti-Pattern: Hard-Coded Service Mocks in Property Tests

**This is what NOT to do.** The following pattern defeats the purpose of property-based testing because it uses hard-coded values and `Effect.fail("Not implemented")` instead of using Arbitrary to generate varied test data:

```typescript
// ❌ WRONG: Hard-coded values and "Not implemented" errors
const defaultTestLayer = Layer.mergeAll(
  Layer.succeed(ImporterService, {
    import: (identifier: string) =>
      Effect.fail(
        new HalImportError({
          message: `No importer configured for: ${identifier}`,
          identifier,
        }),
      ),
  }),
  Layer.succeed(UserWalletService, {
    // ❌ Hard-coded address - every property test gets the same value
    getAddress: () => Effect.succeed("0xTestWallet1234567890123456789012345678"),
  }),
  Layer.succeed(BlockchainClientService, {
    // ❌ Hard-coded responses - no variation across test runs
    call: () => Effect.succeed("0x"),
    getLogs: () => Effect.succeed([]),
    // ❌ "Not implemented" - these won't be tested at all
    getTransactionTrace: () => Effect.fail(new Error("Not implemented")),
  }),
  Layer.succeed(ContractMetadataService, {
    getFunctionAbi: () => Effect.fail(new Error("Not implemented")),
    getEventAbi: () => Effect.fail(new Error("Not implemented")),
  }),
  Layer.succeed(TransactionService, {
    sendTransaction: () => Effect.fail(new Error("Not implemented")),
  }),
  Layer.succeed(SwapService, {
    executeSwap: () => Effect.fail(new Error("Not implemented")),
  }),
  Layer.succeed(CodeExecutionService, {
    execute: () => Effect.fail(new CodeExecutionNotImplementedError()),
  }),
);
```

**Why this is wrong:**

1. **Hard-coded values** - Property tests run the same inputs every time, missing edge cases
2. **`Effect.fail(new Error("Not implemented"))`** - These code paths are never exercised
3. **No Arbitrary** - The whole point of property testing is generating varied data

**The correct approach:** Use `Arbitrary` inside `Layer.effect` to generate different values for each property test run:

```typescript
import { Arbitrary, Schema, Effect, Layer, Ref } from "effect";
import * as fc from "fast-check";

// Define schemas for your service return types
const WalletAddress = Schema.String.pipe(
  Schema.pattern(/^0x[a-fA-F0-9]{40}$/),
  Schema.annotations({
    arbitrary: () => (fc) => fc.hexaString({ minLength: 40, maxLength: 40 }).map((hex) => `0x${hex}`),
  }),
);

const HexData = Schema.String.pipe(
  Schema.pattern(/^0x[a-fA-F0-9]*$/),
  Schema.annotations({
    arbitrary: () => (fc) => fc.hexaString({ minLength: 0, maxLength: 64 }).map((hex) => `0x${hex}`),
  }),
);

// ✅ CORRECT: Use Arbitrary to generate test data in the layer
const PropertyTestLayer = Layer.effect(
  UserWalletService,
  Effect.sync(() => {
    // Generate a random address for THIS test run
    const address = fc.sample(Arbitrary.make(WalletAddress)(fc), 1)[0];
    return {
      getAddress: () => Effect.succeed(address),
    };
  }),
);

// ✅ CORRECT: For stateful services, combine Ref with Arbitrary
const BlockchainClientTestLayer = Layer.effect(
  BlockchainClientService,
  Effect.gen(function* () {
    // Pre-generate test data for this test run
    const callResultArb = Arbitrary.make(HexData)(fc);
    const logsArb = Arbitrary.make(Schema.Array(EventLog))(fc);

    return {
      call: () => Effect.succeed(fc.sample(callResultArb, 1)[0]),
      getLogs: () => Effect.succeed(fc.sample(logsArb, 1)[0]),
      // ✅ Generate valid trace data instead of failing
      getTransactionTrace: () => Effect.succeed(fc.sample(Arbitrary.make(TransactionTrace)(fc), 1)[0]),
    };
  }),
);

// ✅ CORRECT: Full test layer with Arbitrary-generated data
const FullPropertyTestLayer = Layer.mergeAll(
  PropertyTestLayer,
  BlockchainClientTestLayer,
  // For services you actually want to test failure cases,
  // use Arbitrary to generate the ERROR data too:
  Layer.effect(
    ImporterService,
    Effect.sync(() => ({
      import: (identifier: string) =>
        // ✅ Either succeed with generated data OR fail with generated error
        fc.sample(fc.boolean(), 1)[0]
          ? Effect.succeed(fc.sample(Arbitrary.make(ImportResult)(fc), 1)[0])
          : Effect.fail(fc.sample(Arbitrary.make(HalImportError)(fc), 1)[0]),
    })),
  ),
);
```

**Key principles:**

1. **Every service method should return Arbitrary-generated data** - Not hard-coded strings
2. **Generate errors with Arbitrary too** - Error schemas should produce varied error cases
3. **Use `Layer.effect` + `Effect.sync`** - So each test run gets fresh generated values
4. **If a method "shouldn't be called"** - Either generate valid data anyway, or use a schema-based error

### Combining Services with Property Testing

The ultimate testing pattern: **service test layers + Schema Arbitrary.** This lets you test business logic across thousands of generated inputs with all external dependencies controlled:

```typescript
import { it, expect, layer } from "@effect/vitest";
import { Schema, Arbitrary, Effect } from "effect";

layer(TestEnv)("Order Processing Properties", (it) => {
  it.effect.prop("should calculate correct total for any valid order", [Arbitrary.make(Order)], ([order]) =>
    Effect.gen(function* () {
      const orderRepo = yield* OrderRepository;
      yield* orderRepo.save(order);
      const saved = yield* orderRepo.findById(order.id);
      expect(saved.total).toBe(order.items.reduce((sum, i) => sum + i.price, 0));
    }),
  );

  it.effect.prop("should never charge negative amounts", [Arbitrary.make(Order)], ([order]) =>
    Effect.gen(function* () {
      const gateway = yield* PaymentGateway;
      const result = yield* gateway.charge(order.total, "usd");
      expect(result.amount).toBeGreaterThanOrEqual(0);
    }),
  );
});
```

## Setup

Install `@effect/vitest` alongside vitest (v1.6.0+):

```bash
pnpm add -D vitest @effect/vitest
```

Enable Effect-aware equality in your test setup:

```typescript
// vitest.setup.ts (or at top of test files)
import { addEqualityTesters } from "@effect/vitest";

addEqualityTesters();
```

## @effect/vitest - Core Test Functions

### it.effect - Standard Effect Tests

**Use `it.effect` for all Effect tests.** It automatically provides `TestContext` (including `TestClock`) and runs the Effect to completion. No `async`/`await` or `Effect.runPromise` needed.

```typescript
import { it, expect } from "@effect/vitest";
import { Effect, Schema, Arbitrary } from "effect";

// Use it.effect.prop for property-based testing with generated data
it.effect.prop("should return user for any valid id", [Schema.String.pipe(Schema.minLength(1))], ([userId]) =>
  Effect.gen(function* () {
    const user = yield* getUser(userId);
    expect(user.id).toBe(userId);
  }),
);
```

**NEVER use plain vitest `it` with `Effect.runPromise` when `it.effect` is available:**

```typescript
// ❌ FORBIDDEN: manual Effect.runPromise in async test
import { it, expect } from "vitest";

it("should return user", async () => {
  const result = await Effect.runPromise(getUser(userId));
  expect(result).toBeDefined();
});

// ✅ REQUIRED: it.effect.prop handles execution + data generation automatically
import { it, expect } from "@effect/vitest";
import { Effect, Schema } from "effect";

it.effect.prop("should return user for any valid id", [Schema.String.pipe(Schema.minLength(1))], ([userId]) =>
  Effect.gen(function* () {
    const user = yield* getUser(userId);
    expect(user.id).toBe(userId);
  }),
);
```

### it.scoped - Tests with Resource Management

Use `it.scoped` when your test needs a `Scope` (e.g., acquireRelease resources). The scope is automatically closed when the test ends.

```typescript
import { it, expect } from "@effect/vitest";
import { Effect } from "effect";

it.scoped("should manage database connection", () =>
  Effect.gen(function* () {
    const conn = yield* acquireDbConnection; // acquireRelease resource
    const result = yield* conn.query("SELECT 1");
    expect(result).toBeDefined();
    // Connection is automatically released when test ends
  }),
);
```

### it.live - Tests with Live Environment

Use `it.live` when you need the real runtime environment (real clock, real logger) instead of test services.

```typescript
import { it, expect } from "@effect/vitest";
import { Effect } from "effect";

it.live("should measure real time", () =>
  Effect.gen(function* () {
    const start = Date.now();
    yield* Effect.sleep("10 millis");
    const elapsed = Date.now() - start;
    expect(elapsed).toBeGreaterThanOrEqual(10);
  }),
);
```

- `it.scopedLive` combines scoped resources with the live environment.

### it.layer - Tests with Shared Layers

Use `it.layer` to provide dependencies to a group of tests. The layer is constructed once and shared across all tests in the block. Always use `Layer.effect` with `Ref` for stateful services and Arbitrary for test data.

```typescript
import { it, expect, layer } from "@effect/vitest";
import { Effect, Layer, Context, Ref, Option, Schema, Arbitrary } from "effect";
import * as fc from "fast-check";

// Define User schema for Arbitrary generation
class User extends Schema.Class<User>("User")({
  id: Schema.String.pipe(Schema.minLength(1)),
  name: Schema.String.pipe(Schema.minLength(1)),
  email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/)),
}) {}

class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", {
  userId: Schema.String,
}) {}

class UserRepository extends Context.Tag("UserRepository")<
  UserRepository,
  {
    readonly findById: (id: string) => Effect.Effect<User, UserNotFound>;
    readonly save: (user: User) => Effect.Effect<void>;
  }
>() {}

// Stateful test layer with Ref for storage
const TestUserRepo = Layer.effect(
  UserRepository,
  Effect.gen(function* () {
    const store = yield* Ref.make<Map<string, User>>(new Map());

    return {
      findById: (id: string) =>
        Effect.gen(function* () {
          const users = yield* Ref.get(store);
          return yield* Option.match(Option.fromNullable(users.get(id)), {
            onNone: () => Effect.fail(new UserNotFound({ userId: id })),
            onSome: Effect.succeed,
          });
        }).pipe(Effect.flatten),
      save: (user: User) => Ref.update(store, (m) => new Map(m).set(user.id, user)),
    };
  }),
);

// Top-level layer wrapping a describe block
layer(TestUserRepo)("UserService", (it) => {
  it.effect.prop("should save and find any valid user", [Arbitrary.make(User)], ([user]) =>
    Effect.gen(function* () {
      const repo = yield* UserRepository;
      yield* repo.save(user);
      const found = yield* repo.findById(user.id);
      expect(found).toEqual(user);
    }),
  );

  it.effect.prop("should fail for any non-existent user id", [Schema.String.pipe(Schema.minLength(1))], ([userId]) =>
    Effect.gen(function* () {
      const repo = yield* UserRepository;
      const exit = yield* Effect.exit(repo.findById(userId));
      expect(exit._tag).toBe("Failure");
    }),
  );
});

// Nested layers for composing dependencies
layer(TestUserRepo)("nested layers", (it) => {
  it.layer(AnotherLayer)((it) => {
    it.effect.prop("has both dependencies for any user", [Arbitrary.make(User)], ([user]) =>
      Effect.gen(function* () {
        const repo = yield* UserRepository;
        const other = yield* AnotherService;
        yield* repo.save(user);
        // Both services available with generated data
      }),
    );
  });
});
```

### Test Modifiers

All test variants support standard Vitest modifiers:

```typescript
it.effect.skip("temporarily disabled", () => Effect.sync(() => {}));
it.effect.only("run only this test", () => Effect.sync(() => {}));
it.effect.fails("expected to fail", () => Effect.fail(new Error("expected")));

// Conditional execution
it.effect.skipIf(process.env.CI)("skip on CI", () => Effect.sync(() => {}));
it.effect.runIf(process.env.CI)("only on CI", () => Effect.sync(() => {}));

// Parameterized tests
it.effect.each([1, 2, 3])("processes %d", (num) =>
  Effect.gen(function* () {
    const result = yield* processNumber(num);
    expect(result).toBeGreaterThan(0);
  }),
);
```

### it.flakyTest - Retry Flaky Tests

Use `it.flakyTest` for tests that may not succeed on the first attempt due to timing, external dependencies, or randomness:

```typescript
import { it } from "@effect/vitest";
import { Effect } from "effect";

it.effect("should eventually succeed", () =>
  it.flakyTest(
    unreliableExternalCall(),
    "5 seconds", // Max retry duration
  ),
);
```

## Property-Based Testing

### it.prop - Schema-Aware Property Tests

`it.prop` integrates property-based testing directly into `@effect/vitest`. It accepts Schema types or fast-check Arbitraries.

```typescript
import { it, expect } from "@effect/vitest";
import { Schema } from "effect";

// Array form with positional arguments
it.prop("addition is commutative", [Schema.Int, Schema.Int], ([a, b]) => {
  expect(a + b).toBe(b + a);
});

// Object form with named arguments
it.prop(
  "validates user fields",
  {
    name: Schema.NonEmptyString,
    age: Schema.Number.pipe(Schema.int(), Schema.between(0, 150)),
  },
  ({ name, age }) => {
    expect(name.length).toBeGreaterThan(0);
    expect(age).toBeGreaterThanOrEqual(0);
  },
);

// Effect-based property test
it.effect.prop("async property", [Schema.String], ([input]) =>
  Effect.gen(function* () {
    const result = yield* processInput(input);
    expect(result).toBeDefined();
  }),
);

// With fast-check options
it.prop(
  "with custom runs",
  [Schema.Int],
  ([n]) => {
    expect(n + 0).toBe(n);
  },
  { fastCheck: { numRuns: 1000 } },
);
```

### Schema.Arbitrary - Generating Test Data

Every Schema can generate random valid test data. This is the foundation of Effect testing.

```typescript
import { Schema, Arbitrary } from "effect";
import * as fc from "fast-check";

// Define your schema (you should already have this)
class User extends Schema.Class<User>("User")({
  id: Schema.String.pipe(Schema.minLength(1)),
  name: Schema.String.pipe(Schema.minLength(1)),
  email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/)),
  age: Schema.Number.pipe(Schema.int(), Schema.between(0, 150)),
}) {}

// Create an arbitrary from the schema
const UserArbitrary = Arbitrary.make(User);

// Generate test data
fc.sample(UserArbitrary(fc), 3);
// => [
//   User { id: "a", name: "xyz", email: "foo@bar.com", age: 42 },
//   User { id: "test", name: "b", email: "x@y.z", age: 0 },
//   ...
// ]
```

### With Schema.TaggedClass (Discriminated Unions)

```typescript
import { Schema, Arbitrary, Match } from "effect";
import * as fc from "fast-check";

class Pending extends Schema.TaggedClass<Pending>()("Pending", {
  orderId: Schema.String,
  items: Schema.Array(Schema.String),
}) {}

class Shipped extends Schema.TaggedClass<Shipped>()("Shipped", {
  orderId: Schema.String,
  items: Schema.Array(Schema.String),
  trackingNumber: Schema.String,
}) {}

class Delivered extends Schema.TaggedClass<Delivered>()("Delivered", {
  orderId: Schema.String,
  items: Schema.Array(Schema.String),
  deliveredAt: Schema.Date,
}) {}

const Order = Schema.Union(Pending, Shipped, Delivered);
type Order = Schema.Schema.Type<typeof Order>;

// Arbitrary for the entire union - generates all variants
const OrderArbitrary = Arbitrary.make(Order);

// Arbitrary for specific variants
const PendingArbitrary = Arbitrary.make(Pending);
const ShippedArbitrary = Arbitrary.make(Shipped);
```

### Customizing Arbitrary Generation

Use `Schema.annotations` to control generated values:

```typescript
const Email = Schema.String.pipe(
  Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/),
  Schema.annotations({
    arbitrary: () => (fc) => fc.emailAddress(), // Use fast-check's email generator
  }),
);

const UserId = Schema.String.pipe(
  Schema.minLength(1),
  Schema.annotations({
    arbitrary: () => (fc) => fc.uuid(), // Generate UUIDs
  }),
);

const Age = Schema.Number.pipe(
  Schema.int(),
  Schema.between(18, 100),
  Schema.annotations({
    arbitrary: () => (fc) => fc.integer({ min: 18, max: 100 }),
  }),
);
```

## Property-Based Testing Patterns

### Round-Trip Testing (Encode/Decode)

Every Schema should pass round-trip testing:

```typescript
import { it, expect } from "@effect/vitest";
import { Schema, Arbitrary } from "effect";
import * as fc from "fast-check";

describe("User schema", () => {
  const UserArbitrary = Arbitrary.make(User);

  it.prop("should survive encode/decode round-trip", [UserArbitrary], ([user]) => {
    const encoded = Schema.encodeSync(User)(user);
    const decoded = Schema.decodeUnknownSync(User)(encoded);

    // Verify structural equality
    expect(decoded).toEqual(user);

    // Verify it's still a User instance
    expect(decoded).toBeInstanceOf(User);
  });
});
```

### Testing Discriminated Unions Exhaustively

```typescript
describe("Order processing", () => {
  it.prop("should handle all order states", [Arbitrary.make(Order)], ([order]) => {
    // This must handle ALL variants - Match.exhaustive ensures it
    const result = Match.value(order).pipe(
      Match.when(Schema.is(Pending), (o) => `Pending: ${o.orderId}`),
      Match.when(Schema.is(Shipped), (o) => `Shipped: ${o.trackingNumber}`),
      Match.when(Schema.is(Delivered), (o) => `Delivered: ${o.deliveredAt}`),
      Match.exhaustive,
    );

    expect(typeof result).toBe("string");
  });
});
```

### Testing Invariants

```typescript
class BankAccount extends Schema.Class<BankAccount>("BankAccount")({
  id: Schema.String,
  balance: Schema.Number.pipe(Schema.nonNegative()),
  transactions: Schema.Array(Schema.Number),
}) {
  get computedBalance() {
    return this.transactions.reduce((sum, t) => sum + t, 0);
  }
}

describe("BankAccount invariants", () => {
  it.prop("should never have negative balance", [Arbitrary.make(BankAccount)], ([account]) => {
    expect(account.balance).toBeGreaterThanOrEqual(0);
  });
});
```

### Testing Transformations

```typescript
const MoneyFromCents = Schema.transform(Schema.Number.pipe(Schema.int()), Schema.Number, {
  decode: (cents) => cents / 100,
  encode: (dollars) => Math.round(dollars * 100),
});

describe("Money transformation", () => {
  it.prop("should preserve value through transform", [Schema.Int.pipe(Schema.between(0, 1000000))], ([cents]) => {
    const dollars = Schema.decodeSync(MoneyFromCents)(cents);
    const backToCents = Schema.encodeSync(MoneyFromCents)(dollars);

    // Allow for floating point rounding
    expect(backToCents).toBe(cents);
  });
});
```

### Testing Idempotency

```typescript
describe("Normalization is idempotent", () => {
  it.prop("normalizing twice equals normalizing once", [Arbitrary.make(Email)], ([email]) => {
    const once = normalizeEmail(email);
    const twice = normalizeEmail(normalizeEmail(email));
    expect(twice).toBe(once);
  });
});
```

### Testing Commutativity

```typescript
describe("Order total calculation", () => {
  it.prop("total is independent of item order", { items: Schema.Array(OrderItem) }, ({ items }) => {
    const total1 = calculateTotal(items);
    const total2 = calculateTotal([...items].reverse());
    expect(total1).toBe(total2);
  });
});
```

## Testing Services with Layers

Combine `it.layer` with Arbitrary for service-level testing — **all test data comes from Schemas**:

```typescript
import { it, expect, layer } from "@effect/vitest";
import { Effect, Layer, Context, Ref, Option, Schema, Arbitrary } from "effect";

// Define schemas for all domain types
class User extends Schema.Class<User>("User")({
  id: Schema.String.pipe(Schema.minLength(1)),
  name: Schema.String.pipe(Schema.minLength(1)),
  email: Schema.String.pipe(Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/)),
  age: Schema.Number.pipe(Schema.int(), Schema.between(0, 150)),
}) {}

class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", {
  userId: Schema.String,
}) {}

class UserRepository extends Context.Tag("UserRepository")<
  UserRepository,
  {
    readonly save: (user: User) => Effect.Effect<void>;
    readonly findById: (id: string) => Effect.Effect<User, UserNotFound>;
  }
>() {}

// Stateful test layer
const TestUserRepo = Layer.effect(
  UserRepository,
  Effect.gen(function* () {
    const usersRef = yield* Ref.make<Map<string, User>>(new Map());

    return {
      save: (user: User) => Ref.update(usersRef, (users) => new Map(users).set(user.id, user)),

      findById: (id: string) =>
        Effect.gen(function* () {
          const users = yield* Ref.get(usersRef);
          const user = users.get(id);
          return yield* Option.match(Option.fromNullable(user), {
            onNone: () => Effect.fail(new UserNotFound({ userId: id })),
            onSome: Effect.succeed,
          });
        }).pipe(Effect.flatten),
    };
  }),
);

layer(TestUserRepo)("UserService", (it) => {
  it.effect.prop("should save and retrieve any valid user", [Arbitrary.make(User)], ([user]) =>
    Effect.gen(function* () {
      const repo = yield* UserRepository;
      yield* repo.save(user);
      const found = yield* repo.findById(user.id);
      expect(found).toEqual(user);
    }),
  );

  it.effect.prop("should fail for any non-existent user id", [Schema.String.pipe(Schema.minLength(1))], ([userId]) =>
    Effect.gen(function* () {
      const repo = yield* UserRepository;
      const exit = yield* Effect.exit(repo.findById(userId));
      expect(exit._tag).toBe("Failure");
    }),
  );
});
```

## Testing Error Handling

```typescript
import { it, expect } from "@effect/vitest";
import { Effect, Schema, Match, Arbitrary } from "effect";

class ValidationError extends Schema.TaggedError<ValidationError>()("ValidationError", {
  field: Schema.String,
  message: Schema.String,
}) {}

class NotFoundError extends Schema.TaggedError<NotFoundError>()("NotFoundError", { resourceId: Schema.String }) {}

const AppError = Schema.Union(ValidationError, NotFoundError);
type AppError = Schema.Schema.Type<typeof AppError>;

describe("Error handling", () => {
  it.prop("should handle all error types", [Arbitrary.make(AppError)], ([error]) => {
    const message = Match.value(error).pipe(
      Match.tag("ValidationError", (e) => `Validation: ${e.field}`),
      Match.tag("NotFoundError", (e) => `Not found: ${e.resourceId}`),
      Match.exhaustive,
    );

    expect(typeof message).toBe("string");
  });
});
```

## Testing with Exit

Use `Effect.exit` within `it.effect.prop` to inspect success/failure outcomes with generated error data:

```typescript
import { it, expect } from "@effect/vitest";
import { Effect, Exit, Cause, Option, Schema, Arbitrary } from "effect";

class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", {
  userId: Schema.String.pipe(Schema.minLength(1)),
}) {}

it.effect.prop("should fail with UserNotFound for any generated error", [Arbitrary.make(UserNotFound)], ([error]) =>
  Effect.gen(function* () {
    const exit = yield* Effect.exit(Effect.fail(error));

    expect(Exit.isFailure(exit)).toBe(true);

    Exit.match(exit, {
      onFailure: (cause) => {
        const failureError = Cause.failureOption(cause);
        expect(Option.isSome(failureError)).toBe(true);
        expect(Schema.is(UserNotFound)(Option.getOrThrow(failureError))).toBe(true);
        expect(Option.getOrThrow(failureError).userId).toBe(error.userId);
      },
      onSuccess: () => {
        throw new Error("Expected failure");
      },
    });
  }),
);
```

## TestClock - Controlling Time

`it.effect` automatically provides `TestClock`. No need to manually provide `TestClock.layer`.

### Basic Time Control

```typescript
import { it, expect } from "@effect/vitest";
import { Effect, TestClock, Fiber } from "effect";

it.effect("should complete after simulated delay", () =>
  Effect.gen(function* () {
    const fiber = yield* Effect.fork(Effect.sleep("1 hour").pipe(Effect.as("completed")));

    yield* TestClock.adjust("1 hour");

    const result = yield* Fiber.join(fiber);
    expect(result).toBe("completed");
  }),
);
```

### Testing Scheduled Operations

```typescript
it.effect("should retry 3 times", () =>
  Effect.gen(function* () {
    let attempts = 0;

    const fiber = yield* Effect.fork(
      Effect.sync(() => {
        attempts++;
      }).pipe(
        Effect.flatMap(() => Effect.fail("error")),
        Effect.retry(Schedule.spaced("1 second").pipe(Schedule.recurs(3))),
      ),
    );

    yield* TestClock.adjust("1 second");
    yield* TestClock.adjust("1 second");
    yield* TestClock.adjust("1 second");

    yield* Fiber.join(fiber).pipe(Effect.ignore);

    expect(attempts).toBe(4); // Initial + 3 retries
  }),
);
```

## Testing Configuration

```typescript
import { it, expect } from "@effect/vitest";
import { Effect, Config, ConfigProvider, Layer } from "effect";

const TestConfigProvider = ConfigProvider.fromMap(
  new Map([
    ["DATABASE_URL", "postgres://test:test@localhost/test"],
    ["API_KEY", "test-api-key"],
    ["DEBUG", "true"],
  ]),
);

const TestConfig = Layer.setConfigProvider(TestConfigProvider);

it.effect("should use test configuration", () =>
  Effect.gen(function* () {
    const dbUrl = yield* Config.string("DATABASE_URL");
    const debug = yield* Config.boolean("DEBUG");
    expect(dbUrl).toContain("localhost/test");
    expect(debug).toBe(true);
  }).pipe(Effect.provide(TestConfig)),
);
```

## @effect/vitest Quick Reference

| Function         | Environment            | Scope        | Use Case                                     |
| ---------------- | ---------------------- | ------------ | -------------------------------------------- |
| `it.effect`      | Test (TestClock, etc.) | No           | Most common; deterministic tests             |
| `it.live`        | Live (real clock)      | No           | Tests needing real environment               |
| `it.scoped`      | Test                   | Auto-cleanup | Tests with acquireRelease resources          |
| `it.scopedLive`  | Live                   | Auto-cleanup | Resources + real environment                 |
| `it.layer`       | Test                   | Shared layer | Providing dependencies to test groups        |
| `it.prop`        | Sync                   | No           | Property-based testing with Schema/Arbitrary |
| `it.effect.prop` | Test                   | No           | Effect-based property testing                |
| `it.flakyTest`   | Test                   | No           | Retry flaky tests with timeout               |

## Best Practices

### Do

1. **Wrap ALL external dependencies in Services** - Every API call, database query, file read, and third-party SDK call MUST go through a `Context.Tag` service. This is the foundation of testable Effect code.
2. **Create test Layers for every Service** - Every service MUST have a corresponding test implementation. Use `Layer.succeed` for simple mocks, `Layer.effect` with `Ref` for stateful mocks.
3. **Use `it.layer` to provide test services** - Share test layers across test blocks. Compose multiple test layers with `Layer.merge`.
4. **Use `@effect/vitest` for ALL Effect tests** - Never use plain vitest `it` with `Effect.runPromise`
5. **Use `it.effect` as the default** - It provides TestContext automatically
6. **Use `it.prop` or `it.effect.prop` for property tests** - Prefer over manual `fc.assert`/`fc.property`. Combine with service test layers for full coverage.
7. **Use Arbitrary for ALL test data** - Never hand-craft test objects. Every Schema generates Arbitrary data.
8. **Combine services + Arbitrary for 100% coverage** - Service test layers control all I/O; Arbitrary generates all data. Together they eliminate every testing gap.
9. **Test round-trips for every Schema** - Encode/decode should be lossless
10. **Test all union variants** - Use Match.exhaustive to ensure coverage
11. **Test invariants with properties** - Not just specific examples
12. **Generate errors too** - Use Arbitrary on error schemas
13. **Use TestClock for time** - Deterministic, fast tests (automatic in `it.effect`)
14. **Call `addEqualityTesters()`** - For proper Effect type equality in assertions

### Don't

1. **Don't call external APIs/databases directly in business logic** - Always go through a Service. Direct calls make code untestable.
2. **Don't skip writing test Layers** - Every Service needs a test Layer. If a service doesn't have one, your coverage is incomplete.
3. **Don't use `Effect.runPromise` in tests** - Use `it.effect` instead
4. **Don't import `it` from `vitest`** - Import from `@effect/vitest`
5. **Don't hard-code test data** - Use Arbitrary.make(YourSchema) or `it.prop`
6. **Don't test just "happy path"** - Generate edge cases automatically
7. **Don't use `._tag` directly** - Use Match.tag or Schema.is() (for Schema types only)
8. **Don't skip round-trip tests** - They catch serialization bugs
9. **Don't manually provide TestClock** - `it.effect` does this automatically

### The Path to 100% Test Coverage

```
1. Define Services (Context.Tag)     → Every external dependency has an interface
2. Create Test Layers               → Every service has a test implementation
3. Use it.layer / layer()           → Tests compose all test layers automatically
4. Use Arbitrary + it.prop          → Test data is generated, not hand-crafted
5. Test error paths with Arbitrary  → Error schemas generate error cases too
6. Te

…(truncated)
