tRPC 라우터 통합 테스트
서로 다른 tRPC 절차와 해당 의존성이 올바르게 상호작용하는지 확인하는 통합 테스트를 설정합니다.
tRPC 라우터 통합 테스트은(는) CoddyKit의 무료 tRPC End-to-End Type Safe APIs 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 tRPC End-to-End Type Safe APIs 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. tRPC End-to-End Type Safe APIs 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
What are Integration Tests?
Welcome to integration testing for tRPC! While unit tests check individual functions, integration tests verify how different parts of your tRPC application work together.
This often involves testing the interaction between tRPC procedures and external dependencies, like a database or another service.
Unit vs. Integration Tests
Let's clarify the difference:
- Unit Tests: Focus on isolating a single tRPC procedure. They mock all external dependencies to test the procedure's logic alone.
- Integration Tests: Test the flow between multiple procedures or between a procedure and a real (or mocked) external dependency, like ensuring a mutation correctly updates a database.
Setting Up for Integration Tests
For integration tests, you need a controlled environment. This often means:
- Using an in-memory database or a dedicated test database instance.
- Mocking only necessary external services, not core dependencies like your database.
- Setting up a tRPC test caller to invoke procedures directly, bypassing HTTP requests.
Creating a tRPC Test Caller
To call your tRPC procedures directly within tests, you create a test caller. This object lets you interact with your router without a full HTTP server, making tests faster and easier to set up.
Try running this example to see a basic caller in action:
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.context<any>().create();
const publicProcedure = t.procedure;
const appRouter = t.router({
greeting: publicProcedure
.input(z.object({ name: z.string() }).optional())
.query(({ input }) => {
return `Hello, ${input?.name || 'world'}!`;
}),
addNumber: publicProcedure
.input(z.object({ num1: z.number(), num2: z.number() }))
.mutation(({ input }) => {
return input.num1 + input.num2;
}),
});
const caller = appRouter.createCaller({});
async function runSimulatedTest() {
console.log("Simulating tRPC integration test...");
const greetingResult = await caller.greeting.query({ name: "Coddy" });
console.log("Greeting result:", greetingResult);
const sumResult = await caller.addNumber.mutation({ num1: 5, num2: 3 });
console.log("Sum result:", sumResult);
}
runSimulatedTest();Testing a Query Procedure
Integration tests for queries verify that data is fetched correctly, often involving a mock database. We'll ensure the query returns the expected data based on the current state.
Observe how the greeting query behaves with and without input:
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.context<any>().create();
const publicProcedure = t.procedure;
const appRouter = t.router({
greeting: publicProcedure
.input(z.object({ name: z.string() }).optional())
.query(({ input }) => {
// In a real test, this might fetch from a DB
return `Hello, ${input?.name || 'world'}!`;
}),
});
const caller = appRouter.createCaller({});
async function testGreetingQuery() {
console.log("--- Testing 'greeting' query ---");
// Test with a name
const resultWithName = await caller.greeting.query({ name: "Alice" });
console.log("Result with name:", resultWithName);
// Test without a name (default behavior)
const resultNoName = await caller.greeting.query();
console.log("Result no name:", resultNoName);
console.log("Query tests complete.");
}
testGreetingQuery();Testing a Mutation Procedure
Mutations change data. An integration test for a mutation should verify that the data changes as expected and that subsequent queries reflect those changes. We'll simulate an in-memory database.
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.context<any>().create();
const publicProcedure = t.procedure;
// Simulate an in-memory database
const _db: { messages: string[] } = { messages: [] };
const appRouter = t.router({
getMessages: publicProcedure
.query(() => _db.messages),
addMessage: publicProcedure
.input(z.object({ text: z.string() }))
.mutation(({ input }) => {
_db.messages.push(input.text);
return input.text;
}),
});
const caller = appRouter.createCaller({});
async function testAddMessageMutation() {
console.log("--- Testing 'addMessage' mutation ---");
// 1. Check initial state
let initialMessages = await caller.getMessages.query();
console.log("Initial messages:", initialMessages);
// 2. Perform mutation
const addedText = await caller.addMessage.mutation({ text: "Hello tRPC!" });
console.log("Added message:", addedText);
// 3. Check state after mutation
let updatedMessages = await caller.getMessages.query();
console.log("Updated messages:", updatedMessages);
// 4. Perform another mutation
await caller.addMessage.mutation({ text: "More data!" });
updatedMessages = await caller.getMessages.query();
console.log("Further updated messages:", updatedMessages);
console.log("Mutation tests complete.");
}
testAddMessageMutation();Verifying Merged Routers
In larger tRPC applications, you'll organize your API into multiple routers and then merge them into a root router. Integration tests should confirm that procedures from different merged routers can be accessed and interact correctly.
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.context<any>().create();
const publicProcedure = t.procedure;
// Simulate an in-memory database
const _db: { users: string[]; products: string[] } = { users: [], products: [] };
// User router
const userRouter = t.router({
getUsers: publicProcedure.query(() => _db.users),
addUser: publicProcedure
.input(z.object({ name: z.string() }))
.mutation(({ input }) => {
_db.users.push(input.name);
return input.name;
}),
});
// Product router
const productRouter = t.router({
getProducts: publicProcedure.query(() => _db.products),
addProduct: publicProcedure
.input(z.object({ name: z.string() }))
.mutation(({ input }) => {
_db.products.push(input.name);
return input.name;
}),
});
// Root router merging user and product routers
const appRouter = t.router({
user: userRouter,
product: productRouter,
});
const caller = appRouter.createCaller({});
async function testMergedRouters() {
console.log("--- Testing merged routers ---");
// Add a user
await caller.user.addUser.mutation({ name: "Bob" });
const users = await caller.user.getUsers.query();
console.log("Users after adding Bob:", users);
// Add a product
await caller.product.addProduct.mutation({ name: "Laptop" });
const products = await caller.product.getProducts.query();
console.log("Products after adding Laptop:", products);
console.log("All users:", await caller.user.getUsers.query());
console.log("All products:", await caller.product.getProducts.query());
console.log("Merged router tests complete.");
}
testMergedRouters();Testing with Context Data
Many tRPC procedures rely on the context object, which might contain data like an authenticated user's ID or roles. In integration tests, you'll provide a mock context when creating your caller to simulate different user states.
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
// Define the shape of your context
interface MyContext {
userId?: string;
isAdmin?: boolean;
}
const t = initTRPC.context<MyContext>().create();
const publicProcedure = t.procedure;
const protectedProcedure = publicProcedure.use(
t.middleware(({ ctx, next }) => {
if (!ctx.userId) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authenticated' });
}
return next({ ctx: { ...ctx, userId: ctx.userId } });
})
);
const adminProcedure = protectedProcedure.use(
t.middleware(({ ctx, next }) => {
if (!ctx.isAdmin) {
throw new TRPCError({ code: 'FORBIDDEN', message: 'Not an admin' });
}
return next({ ctx: { ...ctx, isAdmin: ctx.isAdmin } });
})
);
const appRouter = t.router({
whoAmI: protectedProcedure.query(({ ctx }) => `You are user ${ctx.userId}`),
adminAction: adminProcedure.mutation(() => "Admin action successful!"),
});
async function testWithMockContext() {
console.log("--- Testing with mock context ---");
// 1. Test unauthenticated user
const unauthCaller = appRouter.createCaller({});
try {
await unauthCaller.whoAmI.query();
} catch (error: any) {
console.log("Unauthenticated user error:", error.message);
}
// 2. Test authenticated user
const authCaller = appRouter.createCaller({ userId: "user123" });
const userResult = await authCaller.whoAmI.query();
console.log("Authenticated user result:", userResult);
// 3. Test non-admin user trying admin action
try {
await authCaller.adminAction.mutation();
} catch (error: any) {
console.log("Non-admin user error:", error.message);
}
// 4. Test admin user
const adminCaller = appRouter.createCaller({ userId: "admin456", isAdmin: true });
const adminResult = await adminCaller.adminAction.mutation();
console.log("Admin user result:", adminResult);
console.log("Context tests complete.");
}
testWithMockContext();Cleaning Up Test Data
Integration tests that modify shared state (like a database) can create dependencies between tests if not managed carefully. It's crucial to clean up any created data after each test or test suite.
- Use
beforeEach/afterEachhooks in your test runner. - Reset mock databases or truncate tables to ensure tests are isolated and repeatable.
Integration Test Challenge
Consider a tRPC setup where a userRouter handles user creation and a postRouter handles post creation, with posts requiring a valid userId. You are writing an integration test.
Recap: Integration Testing tRPC
We've explored how to perform integration tests for tRPC routers. You learned to:
- Use a test caller to invoke procedures directly.
- Test both query and mutation procedures, observing state changes.
- Verify interactions across merged routers.
- Provide mock context for authenticated tests.
- Understand the importance of test data cleanup.
Integration tests are vital for ensuring your tRPC API's components work harmoniously and correctly interact with dependencies.
자주 묻는 질문
“tRPC 라우터 통합 테스트” 강의는 무료인가요?
네 — “tRPC 라우터 통합 테스트” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 tRPC End-to-End Type Safe APIs 강의 전체를 잠금 해제할 수 있습니다. tRPC End-to-End Type Safe APIs 강의에는 총 4개의 강의가 포함되어 있습니다.
“tRPC 라우터 통합 테스트”에서 뭘 배우나요?
서로 다른 tRPC 절차와 해당 의존성이 올바르게 상호작용하는지 확인하는 통합 테스트를 설정합니다. 브라우저에서 직접 실행하는 실습 코드로 tRPC End-to-End Type Safe APIs을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
tRPC End-to-End Type Safe APIs을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 tRPC End-to-End Type Safe APIs은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“tRPC 라우터 통합 테스트” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 tRPC End-to-End Type Safe APIs 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 tRPC End-to-End Type Safe APIs 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- tRPC 절차 단위 테스트
- tRPC 라우터 통합 테스트
- 종단 간 테스트 전략
- tRPC 테스트에서 컨텍스트와 종속성 모킹하기