Testing & Mocking
Test Silgi procedures without starting a server.
Three patterns for testing Silgi APIs, from lightweight unit tests to full HTTP-level integration tests.
1. callable() — unit testing
Test a single procedure in isolation. The compiled pipeline runs (guards, wraps, validation), but there is no HTTP involved.
import { } from 'silgi'
import { , , } from 'vitest'
const = k
.$input(z.object({ : z.number().optional() }))
.$resolve(({ , }) => .db.users.findMany({ : .limit }))
const = (, {
: () => ({
: createTestDB(),
}),
})
('listUsers', () => {
('returns users with limit', async () => {
const = await ({ : 2 })
().(2)
})
('rejects invalid input', async () => {
// @ts-expect-error — testing runtime validation
await (({ : 'bad' }))..()
})
})callable() is the fastest way to test. Use it when you want to verify a procedure's logic and validation without worrying about HTTP serialization.
Testing guarded procedures
Guards run as part of the compiled pipeline. Provide the context they need:
const = k
.$use(auth)
.$input(z.object({ : z.string() }))
.$resolve(({ , }) => .db.users.create())
const = callable(, {
: () => ({
: { : 'Bearer test-token' },
: createTestDB(),
}),
})
it('creates a user when authenticated', async () => {
const = await ({ : 'Alice' })
expect(.name).toBe('Alice')
})To test that a guard rejects correctly, omit the required context:
const = callable(createUser, {
: () => ({
: {},
: createTestDB(),
}),
})
it('rejects unauthenticated requests', async () => {
await expect(({ : 'Alice' })).rejects.toThrow('UNAUTHORIZED')
})2. createServerClient() — integration testing
Test the full router as a single unit. The server client compiles the router once and lets you call any procedure by path — same as the real client, but in-process.
import { } from 'silgi/client/server'
const = (appRouter, {
: () => ({
: createTestDB(),
: { : 'Bearer test-token' },
}),
})
describe('user flow', () => {
it('creates and lists users', async () => {
await .users.create({ : 'Alice' })
await .users.create({ : 'Bob' })
const = await .users.list({ : 10 })
expect().toHaveLength(2)
expect([0].name).toBe('Alice')
})
})This is the best pattern for integration tests. You exercise the full pipeline — guards, wraps, validation, routing — without starting a server or dealing with ports.
createServerClient() reuses the compiled handler for every call. It is fast enough to use in large test suites
without worrying about setup overhead.
3. handler() + fetch() — HTTP-level testing
When you need to test the actual HTTP layer — status codes, headers, content negotiation, CORS — use handler() with the Fetch API:
const = s.handler(appRouter)
describe('HTTP behavior', () => {
it('returns 200 with JSON', async () => {
const = await (
new ('http://localhost/users/list', {
: 'POST',
: { 'Content-Type': 'application/json' },
: .({ : 5 }),
}),
)
expect(.status).toBe(200)
expect(.headers.get('content-type')).toContain('application/json')
const = await .json()
expect().toHaveLength(5)
})
it('returns 400 for invalid input', async () => {
const = await (
new ('http://localhost/users/list', {
: 'POST',
: { 'Content-Type': 'application/json' },
: .({ : 'not-a-number' }),
}),
)
expect(.status).toBe(400)
})
it('negotiates MessagePack', async () => {
const = await (
new ('http://localhost/users/list', {
: 'POST',
: {
'Content-Type': 'application/json',
: 'application/x-msgpack',
},
: .({ : 5 }),
}),
)
expect(.headers.get('content-type')).toContain('application/x-msgpack')
})
})No server process, no port allocation. The handler() function takes a Request and returns a Response — the same Web API you use in production.
Which pattern to use
| Pattern | Best for | Speed | What it tests |
|---|---|---|---|
callable() | Unit tests | Fastest | Procedure logic, validation, guards |
createServerClient() | Integration tests | Fast | Full pipeline, routing, procedure interactions |
handler() + fetch() | HTTP tests | Medium | Status codes, headers, content negotiation |
Start with callable() for most tests. Move to createServerClient() when you need to test across procedures. Use handler() only when the HTTP layer matters.
What's next?
- Server —
callable()andhandler()reference - Client —
createServerClient()reference - Monorepo Setup — share types between packages for testing