# Test Client

In-memory test client with mock support and request history -- no real HTTP calls.

## Overview

`createTestClient` creates an in-memory client that replaces real HTTP calls with configurable mocks. It records every request in a history array, making it straightforward to assert what your code called and with which arguments.

```ts title="install"
import { createTestClient } from "@noverax/courier/testing"
```

## Creating a Test Client

Pass your API type as a generic parameter. The test client exposes the same dot-notation API as `createCaller` / `createProxyCaller`:

```ts title="tests/users.test.ts"
import { createTestClient } from "@noverax/courier/testing"
import type { api } from "../lib/api"

const { client, mock, history, reset } = createTestClient<typeof api>()
```

## API Reference

```ts title="types"
type TestClient<TApi extends DefinedApi = DefinedApi> = {
  /** Typed client -- same dot-notation as createCaller / createProxyCaller */
  client: TypedClient<TApi>

  /** Mock a specific endpoint's response: the group and method are checked, and the builder is typed by its response and variables */
  mock: (group: string, method: string) => MockBuilder

  /** Array of all recorded requests */
  history: HistoryEntry[]

  /** Clear all mocks and history */
  reset(): void

  /** Clear only mocks */
  resetMocks(): void

  /** Clear only history */
  resetHistory(): void
}
```

## Basic Usage

Set up a mock, call the endpoint through the client, and assert the result:

```ts title="tests/users.test.ts"
import { describe, it, expect, beforeEach } from "vitest"
import { createTestClient } from "@noverax/courier/testing"
import type { api } from "../lib/api"

describe("users", () => {
  const { client, mock, history, reset } = createTestClient<typeof api>()

  beforeEach(() => {
    reset() // clear mocks and history between tests
  })

  it("fetches all users", async () => {
    const users = [
      { id: 1, name: "Alice" },
      { id: 2, name: "Bob" },
    ]

    mock("users", "getAll").resolves(users)

    const result = await client.users.getAll()

    expect(result).toEqual(users)
    expect(history).toHaveLength(1)
    expect(history[0].method).toBe("getAll")
  })

  it("creates a user with variables", async () => {
    mock("users", "create").resolves({ id: 3, name: "Charlie" })

    const result = await client.users.create({
      name: "Charlie",
      email: "charlie@test.com",
    })

    expect(result).toEqual({ id: 3, name: "Charlie" })
    expect(history[0].variables).toEqual({
      name: "Charlie",
      email: "charlie@test.com",
    })
  })
})
```

> **Mocks are required**
>
> If you call an endpoint without setting up a mock first, the test client throws an error with a helpful message: `[courier/testing] No mock for "users.getAll". Call mock("users", "getAll").resolves(data) first.`

## The Client

The `client` returned by `createTestClient` has the exact same dot-notation API as `createCaller` and `createProxyCaller`:

```ts
// Same signature as production code
const result = await client.users.getById({ id: 1 })

// .raw and .with() work too
const response = await client.users.raw.getById({ id: 1 }) // { status: 200, headers, data }
```

This means you can use the test client anywhere your production code uses a typed client.

## Reset Methods

| Method | Clears mocks | Clears history |
|---|---|---|
| `reset()` | Yes | Yes |
| `resetMocks()` | Yes | No |
| `resetHistory()` | No | Yes |

Use `reset()` in `beforeEach` for a clean slate. Use `resetHistory()` when you want to keep mocks but verify only new requests.

## History Entries

Every call through the client is recorded in the `history` array, regardless of whether it succeeds or fails:

```ts title="types"
type HistoryEntry = {
  group: string        // "users"
  method: string       // "getAll"
  variables?: unknown  // { id: 1 }
  result?: {
    status: number     // 200
    data: unknown      // the mock response
  }
  error?: Error        // set if the mock threw
  timestamp: number    // Date.now()
}
```

### Filtering history

Use the `findRequests` helper to filter history entries by group and method:

```ts title="tests/users.test.ts"
import { createTestClient, findRequests } from "@noverax/courier/testing"

const { client, mock, history } = createTestClient<typeof api>()

mock("users", "getAll").resolves([])
mock("users", "getById").resolves({ id: 1, name: "Alice" })

await client.users.getAll()
await client.users.getById({ id: 1 })
await client.users.getAll()

// Filter by group only
const allUserCalls = findRequests(history, "users")
expect(allUserCalls).toHaveLength(3)

// Filter by group and method
const getAllCalls = findRequests(history, "users", "getAll")
expect(getAllCalls).toHaveLength(2)
```

## Testing Error Handling

Mock a failing endpoint with `.rejects()` and verify your error handling code works:

```ts title="tests/error-handling.test.ts"
import { isHTTPError } from "@noverax/courier"

it("handles 404 errors", async () => {
  mock("users", "getById").rejects(404, { message: "User not found" })

  try {
    await client.users.getById({ id: 999 })
    expect.fail("should have thrown")
  } catch (error) {
    expect(isHTTPError(error)).toBe(true)
    if (isHTTPError(error)) {
      expect(error.status).toBe(404)
      expect(error.data).toEqual({ message: "User not found" })
    }
  }
})
```

## Next Steps

- [Mocking](/docs/testing/mocking) -- full MockBuilder API reference with `.once()`, `.times()`, `.delay()`, and LIFO behavior
