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.
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:
import { createTestClient } from "@noverax/courier/testing"
import type { api } from "../lib/api"
const { client, mock, history, reset } = createTestClient<typeof api>()API Reference
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:
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:
// 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:
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:
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:
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