Skip to content

TestingTest Client

Test Client

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

2 min readdirect mode

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.

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:

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

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:

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

MethodClears mocksClears history
reset()YesYes
resetMocks()YesNo
resetHistory()NoYes

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:

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:

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:

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 -- full MockBuilder API reference with .once(), .times(), .delay(), and LIFO behavior