tRPCMiddleCoding
Как тестировать процедуры tRPC? Какие подходы рекомендуются?
Процедуры тестируются через createCallerFactory с подменным контекстом — это быстро и без HTTP. Для интеграционных тестов используют createTRPCMsw или реальный HTTP-сервер на базе Hono/Express.
Тестирование tRPC-процедур
В tRPC есть три подхода к тестированию: unit-тесты через caller (быстро, без HTTP), интеграционные тесты через HTTP-адаптер и E2E-тесты с MSW-мокированием клиента.
Подход 1: unit-тесты через createCallerFactory
// server/routers/post.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { createCallerFactory } from '../trpc';
import { postRouter } from './post';
import { createMockContext } from '../test-utils';
const createCaller = createCallerFactory(postRouter);
describe('postRouter', () => {
it('list returns published posts', async () => {
const ctx = createMockContext({ user: null });
const caller = createCaller(ctx);
const posts = await caller.list();
expect(posts).toBeInstanceOf(Array);
});
it('create throws UNAUTHORIZED for guests', async () => {
const ctx = createMockContext({ user: null });
const caller = createCaller(ctx);
await expect(
caller.create({ title: 'Test', body: 'Content' })
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
});
it('create works for authenticated user', async () => {
const ctx = createMockContext({
user: { id: 'user-1', name: 'Alice', role: 'USER' },
});
const caller = createCaller(ctx);
const post = await caller.create({ title: 'Hello', body: 'World' });
expect(post.title).toBe('Hello');
expect(post.authorId).toBe('user-1');
});
});
Фабрика тестовых контекстов
// server/test-utils.ts
import { type Context } from './context';
type MockUser = {
id: string;
name: string;
role: 'USER' | 'ADMIN';
} | null;
export function createMockContext(opts: { user: MockUser }): Context {
return {
session: opts.user
? { user: opts.user, expires: '2099-01-01' }
: null,
db: mockPrismaClient, // используйте prisma-mock или vitest mock
};
}
Подход 2: интеграционные тесты через HTTP
// server/integration.test.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './routers/_app';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './routers/_app';
let server: ReturnType<typeof createHTTPServer>;
let client: ReturnType<typeof createTRPCClient<AppRouter>>;
beforeAll(() => {
server = createHTTPServer({
router: appRouter,
createContext: () => ({
session: { user: { id: 'test-user', role: 'USER' } },
db,
}),
});
server.listen(0); // случайный порт
const { port } = server.server.address() as { port: number };
client = createTRPCClient<AppRouter>({
links: [
httpBatchLink({ url: `http://localhost:${port}/trpc` }),
],
});
});
afterAll(() => server.server.close());
it('fetches posts over HTTP', async () => {
const posts = await client.post.list.query();
expect(Array.isArray(posts)).toBe(true);
});
Подход 3: мокирование на клиенте через MSW
// __mocks__/trpc.ts (для тестов React-компонентов)
import { createTRPCMsw } from 'msw-trpc';
import type { AppRouter } from '@/server/routers/_app';
export const trpcMsw = createTRPCMsw<AppRouter>();
// В тесте:
server.use(
trpcMsw.post.list.query(() => [
{ id: '1', title: 'Mocked Post' },
])
);
Подводные камни
- Middleware не тестируются автоматически через caller: если caller вызывается напрямую с контекстом без пользователя, middleware исполнится и бросит ошибку — это ожидаемо, но нужно явно это тестировать.
- Мокируйте db-слой, а не процедуры: тесты, мокирующие сами процедуры, ничего не проверяют — мокируйте Prisma/БД.
- Тип AppRouter на клиенте в тестах: при импорте
AppRouterв тесты React убедитесь, что это только type-импорт — иначе серверный код попадёт в тест. - Состояние базы данных между тестами: используйте транзакции с откатом или отдельную тестовую БД; не полагайтесь на порядок тестов.
- TRPCError в caller-е бросается как есть: используйте
.rejects.toMatchObject({ code: 'UNAUTHORIZED' }), а не.toThrow(). - superjson в тестах: если у вас настроен трансформер, убедитесь что тестовый клиент/caller использует тот же трансформер.
- createCallerFactory не поддерживает subscriptions: для тестирования подписок нужен WebSocket-сервер.
Common mistakes
- Смешивать «тестирование tRPC procedures» с похожим механизмом без критерия выбора.
- Игнорировать риск: неверно оценить границы применения темы «тестирование tRPC procedures» и получить хрупкое решение.
- Показывать только синтаксис и не объяснять поведение в runtime или сборке.
What the interviewer is testing
- Объясняет проверка resolver, middleware, validation и authorization без поднятия браузера.
- Показывает на примере, как работает: обычно тестируют через caller с inner context, отдельно проверяют schema validation и несколько authorization сценариев; e2e поверх HTTP оставляют для интеграции transport и cookies.
- Называет production-нюанс и граничный случай для темы «тестирование tRPC procedures».