# Integration Testing

[Testing services](https://effect.plants.sh/testing/testing-services/) covers the *unit* style: give
each service a fake `layerTest` and test it in isolation. Integration testing is
the opposite end of the spectrum — you run the **real** services, wired together
exactly as in production, and swap out only the one piece you can't (or don't
want to) run for real: usually the lowest-level effectful dependency, like the
database.

The classic case is "test the whole stack against an in-memory database instead
of production Postgres." The good news is that Effect makes this a one-layer
change, *if* you structure the graph so the database is a swappable seam rather
than something baked in.

## The principle: keep the seam open

A service layer that bakes in its dependency with `Layer.provide` **seals** that
dependency — nothing downstream can replace it:

```ts
// Sealed: the Postgres client is baked in. There is no seam to swap.
static readonly layer = UserRepo.layerNoDeps.pipe(Layer.provide(PostgresClient))
```

A layer that leaves the dependency in its requirements (`R`) keeps the seam
**open** — a caller decides what fills it:

```ts
// Open: the layer produces UserRepo but still REQUIRES a SqlClient. The third
// type parameter is the seam.
static readonly layerNoDeps: Layer.Layer<UserRepo, never, SqlClient.SqlClient> = /* ... */
```

So the recipe for integration-testable code is: **define each service with a
dependency-open layer, compose those, and provide the bottom dependency exactly
once — at the edge.** Production fills the seam with the real thing; tests fill
it with a fast one. (This is the same `layerNoDeps` convention from [Composing
Layers](https://effect.plants.sh/services-and-layers/layer-composition/).)

## A stack with the database left open

Two real services: a `UserRepo` that runs SQL, and a `UserService` on top of it.
Neither provides a database — both leave `SqlClient` open.

```ts
import { Context, Effect, Layer } from "effect"
import { SqlClient, SqlError } from "effect/unstable/sql"

interface User {
  readonly id: number
  readonly name: string
}

// Talks real SQL. Its layer REQUIRES a SqlClient but does not provide one —
// that open requirement is the seam we swap.
class UserRepo extends Context.Service<UserRepo, {
  create(name: string): Effect.Effect<User, SqlError.SqlError>
  readonly list: Effect.Effect<ReadonlyArray<User>, SqlError.SqlError>
}>()("app/UserRepo") {
  static readonly layerNoDeps = Layer.effect(
    UserRepo,
    Effect.gen(function*() {
      const sql = yield* SqlClient.SqlClient

      const create = Effect.fn("UserRepo.create")(function*(name: string) {
        const rows = yield* sql<User>`
          INSERT INTO users (name) VALUES (${name}) RETURNING id, name
        `
        return rows[0]
      })

      const list = sql<User>`SELECT id, name FROM users ORDER BY id`

      return UserRepo.of({ create, list })
    })
  )
}

// Higher-level logic. Depends on UserRepo (which transitively needs SqlClient),
// and likewise leaves both open.
class UserService extends Context.Service<UserService, {
  register(name: string): Effect.Effect<User, SqlError.SqlError>
  readonly list: Effect.Effect<ReadonlyArray<User>, SqlError.SqlError>
}>()("app/UserService") {
  static readonly layerNoDeps = Layer.effect(
    UserService,
    Effect.gen(function*() {
      const repo = yield* UserRepo

      const register = Effect.fn("UserService.register")(function*(name: string) {
        return yield* repo.create(name.trim())
      })

      return UserService.of({ register, list: repo.list })
    })
  )
}

// The whole business-logic graph, wired together but with the database still
// OPEN. The one remaining requirement is exactly `SqlClient.SqlClient` — the
// single seam every integration test fills.
const AppServices: Layer.Layer<UserService, never, SqlClient.SqlClient> =
  UserService.layerNoDeps.pipe(Layer.provide(UserRepo.layerNoDeps))
```

Note what `AppServices` is: the real `UserService` and the real `UserRepo`,
composed, with `SqlClient.SqlClient` left in the requirements. The wiring is
final — only the database is missing.

## Two databases, one tag

The swap works because **different SQL clients provide the same service tag.**
Both `PgClient.layer` and `SqliteClient.layer` output `SqlClient.SqlClient`, so a
repository that depends on `SqlClient.SqlClient` doesn't know or care which one
is underneath. Each database layer here also runs its schema migration at build
time (dialects differ, so the DDL does too — more on that below).

```ts
import { Config, Effect, Layer } from "effect"
import { SqlClient } from "effect/unstable/sql"
import { PgClient } from "@effect/sql-pg"
import { SqliteClient } from "@effect/sql-sqlite-node"

// Production: real Postgres from config. `Layer.effectDiscard` runs the schema
// DDL when the layer builds; `provideMerge` builds the client first, feeds it to
// the migration, and re-exposes SqlClient in the output.
const PostgresDatabase = Layer.effectDiscard(
  Effect.gen(function*() {
    const sql = yield* SqlClient.SqlClient
    yield* sql`CREATE TABLE IF NOT EXISTS users (
      id SERIAL PRIMARY KEY,
      name TEXT NOT NULL
    )`
  })
).pipe(
  Layer.provideMerge(PgClient.layerConfig({ url: Config.redacted("DATABASE_URL") }))
)

// Tests: an in-memory SQLite database. `filename: ":memory:"` lives only in RAM,
// so it needs no files and vanishes when the layer is torn down. Same
// `SqlClient.SqlClient` tag — so it satisfies the exact same requirement.
const InMemoryDatabase = Layer.effectDiscard(
  Effect.gen(function*() {
    const sql = yield* SqlClient.SqlClient
    yield* sql`CREATE TABLE users (
      id INTEGER PRIMARY KEY,
      name TEXT NOT NULL
    )`
  })
).pipe(
  Layer.provideMerge(SqliteClient.layer({ filename: ":memory:" }))
)
```

## The swap

Production and test wiring now differ by exactly one layer — the database. Every
service above is untouched.

```ts
import { Layer } from "effect"

// Production: real services, real Postgres.
const AppLayer = AppServices.pipe(Layer.provide(PostgresDatabase))

// Tests: the SAME real services, backed by in-memory SQLite.
const TestAppLayer = AppServices.pipe(Layer.provide(InMemoryDatabase))
```

The integration test then runs `UserService` for real — its logic, the repo's
SQL, the migration, transactions — and asserts through the public interface:

```ts
import { assert, layer } from "@effect/vitest"
import { Effect } from "effect"

layer(TestAppLayer)("UserService (integration)", (it) => {
  it.effect("registers and lists users through the real repo and SQL", () =>
    Effect.gen(function*() {
      const users = yield* UserService

      yield* users.register("  Ada  ") // the real service trims the name
      yield* users.register("Grace")

      const all = yield* users.list
      // Proves the whole stack ran: trimming (service), INSERT/SELECT (repo),
      // ordering and persistence (SQLite). No mocks anywhere.
      assert.deepStrictEqual(all.map((u) => u.name), ["Ada", "Grace"])
    }))
})
```
**Shared vs. fresh database:** `layer(TestAppLayer)(...)` builds the in-memory database **once** and shares it
  across the whole block, so rows from one test are visible to the next — handy
  for a setup-then-assert sequence, but the tests are no longer independent. For
  a clean database per test, provide the layer per test instead
  (`...pipe(Effect.provide(TestAppLayer))`), or wrap it in
  [`Layer.fresh`](https://effect.plants.sh/services-and-layers/layer-composition/#layerfresh) to opt out
  of memoized sharing. This is the same trade-off described in [Testing
  services](https://effect.plants.sh/testing/testing-services/#sharing-a-layer-across-a-block-with-layer).

If a test needs to reach the database directly — to seed fixtures or assert on
raw rows — provide the database with `provideMerge` instead of `provide` so
`SqlClient.SqlClient` stays in the context:

```ts
const TestAppLayerWithSql = AppServices.pipe(Layer.provideMerge(InMemoryDatabase))

layer(TestAppLayerWithSql)("UserService (with raw SQL access)", (it) => {
  it.effect("seeds a row directly, then reads it through the service", () =>
    Effect.gen(function*() {
      const sql = yield* SqlClient.SqlClient
      yield* sql`INSERT INTO users (name) VALUES ('Seeded')`

      const users = yield* UserService
      const all = yield* users.list
      assert.strictEqual(all[0].name, "Seeded")
    }))
})
```

## How faithful is in-memory SQLite?
**Caution:** SQLite is **not** Postgres. Types, functions, constraints, locking, and SQL
  dialect all differ — note the `SERIAL` vs `INTEGER PRIMARY KEY` split in the
  migrations above. In-memory SQLite is fast, zero-setup, and perfect when your
  queries stay portable (or go through a query builder), but it will not catch
  Postgres-specific behavior. The further your SQL leans on Postgres features,
  the more an in-memory substitute lies to you.

When you need true fidelity, swap the leaf for a **real Postgres** instead — the
mechanism is identical, only the database layer changes. A common approach boots
a throwaway Postgres in a container and points `PgClient` at it. Because starting
the container is itself effectful, build the layer with `Layer.unwrap`:

```ts
import { Effect, Layer, Redacted, Scope } from "effect"
import { PgClient } from "@effect/sql-pg"

// Your helper: start a container, return its URL, tear it down with the scope.
declare const startPostgresContainer: Effect.Effect<{ readonly url: string }, never, Scope.Scope>

// A real Postgres, started fresh and torn down with the scope. Still provides
// SqlClient.SqlClient — so it drops into `AppServices` exactly like the others.
const ContainerDatabase = Layer.unwrap(
  Effect.gen(function*() {
    const container = yield* startPostgresContainer
    return PgClient.layer({ url: Redacted.make(container.url) })
  })
)

const ContainerAppLayer = AppServices.pipe(Layer.provide(ContainerDatabase))
```

A useful pattern is to run the *same* integration suite against both: in-memory
SQLite for the fast inner loop, a real Postgres container in CI.

## The pattern beyond databases

Nothing here is database-specific. The seam is just "a low-level service left
open in the graph," and the swap is "provide a different layer for that tag."
The same shape works for:

- An **HTTP client** — run your services for real but provide a stub
  `HttpClient` (via `Layer.succeed`, or
  [`Layer.mock`](https://effect.plants.sh/services-and-layers/layer-composition/#layermock) for a partial
  implementation) that returns canned responses, so tests don't hit the network.
- A **clock** — `it.effect` already provides a deterministic
  [`TestClock`](https://effect.plants.sh/testing/testclock/), so any sleeps, timeouts, and schedules deep
  in your services advance instantly under test.
- **Config** — point a real config-driven layer at test values rather than
  production secrets.

The discipline is the same in every case: keep the thing you want to replace as
an open requirement, compose everything else for real, and fill the seam once at
the edge — with the real implementation in production and a controllable one in
tests.