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».

Sources

Related topics