Testing and Mocking
Learn how to test oRPC procedures directly with server-side clients, test callers, and create mock implementations with the implementer.
Testing
For fast, focused tests, call your procedures directly with call or use createTestCaller from @orpc/server/testing. This lets you verify validation, middleware, and handler logic without going through HTTP.
One-Off Procedure Calls
Use call to invoke a procedure directly with optional context:
import { call } from '@orpc/server'
it('lists planets', async () => {
await expect(
call(router.planet.list, { page: 1, size: 10 })
).resolves.toEqual([
{ id: '1', name: 'Earth' },
{ id: '2', name: 'Mars' },
])
})
Test Callers (createTestCaller)
When procedures require initial context (e.g. database connections or session state), createTestCaller binds context once for your test suite without repeating boilerplate on every call.
import { createTestCaller } from '@orpc/server/testing'
const listPlanets = createTestCaller(router.planet.list, {
context: {
db: mockDatabase,
},
})
it('lists planets with static context', async () => {
const planets = await listPlanets({ page: 1, size: 10 })
expect(planets).toHaveLength(2)
})import { createTestCaller } from '@orpc/server/testing'
const createPlanet = createTestCaller(router.planet.create, {
context: () => ({
requestId: crypto.randomUUID(), // Dynamic fresh UUID per call!
db: mockDatabase,
}),
})
it('creates planets with dynamic request IDs', async () => {
await createPlanet({ name: 'Earth' })
await createPlanet({ name: 'Mars' })
})const getPlanet = createTestCaller(router.planet.find, {
context: { db: defaultMockDb },
})
it('overrides context for a single call', async () => {
// Uses default mock DB
await getPlanet({ id: 'earth' })
// Override context ONLY for this call
await getPlanet(
{ id: 'mars' },
{ context: { db: readOnlyMockDb } }
)
})import { configureTestCaller, createTestCaller } from '@orpc/server/testing'
// Set global test context in test setup file (e.g., vitest.setup.ts)
configureTestCaller({
context: () => ({
db: testDatabaseInstance,
}),
})
it('inherits suite-wide context automatically', async () => {
const listPlanets = createTestCaller(router.planet.list)
await listPlanets({ page: 1 })
})Mocking
Use the Implementer to create test-specific versions of a procedure or router. This is useful when one part of your system depends on another procedure, but your test should not execute the real implementation.
import { function implement<TContract extends RouterContract, TInitialContext extends Context = DefaultInitialContext>(contract: TContract, config?: ProcedureConfig): Implementer<TContract, TInitialContext & object>Turns a contract into an implementer, used to implement the contract's
procedures, routers, and middleware with full type safety.implement } from '@orpc/server'
const const fakeListPlanet: ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>
fakeListPlanet = implement<ImplementedProcedure<{
headers?: IncomingHttpHeaders;
} & object, object, ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>, DefaultInitialContext>(contract: ImplementedProcedure<{
headers?: IncomingHttpHeaders;
} & object, object, ZodObject<...>, ZodArray<...>, object>, config?: ProcedureConfig): Implementer<...>
Turns a contract into an implementer, used to implement the contract's
procedures, routers, and middleware with full type safety.implement(const router: {
planet: {
list: ImplementedProcedure<{
headers?: IncomingHttpHeaders;
} & object, object, ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ImplementedProcedure<{
headers?: IncomingHttpHeaders;
} & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<...>, object>;
create: ImplementedProcedure<...>;
};
}
router.planet: {
list: ImplementedProcedure<{
headers?: IncomingHttpHeaders;
} & object, object, ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>;
find: ImplementedProcedure<{
headers?: IncomingHttpHeaders;
} & object, object, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<...>, object>;
create: ImplementedProcedure<...>;
}
planet.list: ImplementedProcedure<{
headers?: IncomingHttpHeaders;
} & object, object, ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, object>
list).ProcedureImplementer<DefaultInitialContext & object, object, ZodObject<{ limit: ZodOptional<ZodNumber>; cursor: ZodDefault<ZodNumber>; }, $strip>, ZodArray<...>, object>['handler'](handler: ProcedureHandler<DefaultInitialContext & object, {
cursor: number;
limit?: number | undefined;
}, {
id: number;
name: string;
description?: string | undefined;
}[] | AnyORPCError, object>): ImplementedProcedure<DefaultInitialContext & object, object, ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<...>;
}, $strip>>, object>
handler(() => [])
Use fakeListPlanet anywhere your test would normally use the real listPlanet procedure.