Tests unitaires des procédures tRPC
Apprenez à écrire des tests unitaires isolés pour chaque procédure tRPC avec des cadriciels de test populaires.
Tests unitaires des procédures tRPC est une leçon tRPC End-to-End Type Safe APIs gratuite sur CoddyKit. Ceci est la leçon 1 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage tRPC End-to-End Type Safe APIs, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours tRPC End-to-End Type Safe APIs comprend 4 leçons au total.
Certaines parties de cette leçon n'ont pas encore été traduites et s'affichent en anglais.
Unit Testing tRPC Procedures
Welcome! In this lesson, we'll dive into unit testing for tRPC. Unit testing focuses on verifying individual, isolated parts of your code, ensuring they work as expected.
For tRPC, a "unit" often refers to a single procedure (query or mutation). We'll learn how to test these procedures independently, without needing a full server running.
Benefits of Procedure Testing
Unit testing tRPC procedures offers several key advantages:
- Reliability: Catch bugs early in development before they reach production.
- Speed: Tests run quickly, providing instant feedback.
- Isolation: Pinpoint issues to specific procedures, not complex integrations.
- Type Safety: Ensures your procedure's inputs and outputs match your TypeScript definitions.
Essential Tools for tRPC Tests
While you can use various tools, popular choices for TypeScript projects include Jest or Vitest for the test runner. For tRPC backend unit tests, we primarily focus on TypeScript and a test runner's assertion capabilities.
We'll simulate how to call a procedure's internal handler directly for testing, which is the core concept regardless of your chosen test runner.
Deconstructing a Procedure Test
To unit test a tRPC procedure, you need to:
- Import the procedure.
- Create a mock context (
ctx). - Call the procedure's internal handler directly (e.g.,
_def.queryor_def.mutation). - Assert the returned value or any side effects.
Let's see a basic example:
// test-hello.ts
import { initTRPC } from '@trpc/server';
// 1. Define a simple tRPC procedure (or import from your app)
const t = initTRPC.create();
const helloProcedure = t.procedure
.query(() => 'Hello tRPC');
// 2. Create a mock context (empty for this simple case)
const mockContext = {};
async function runHelloTest() {
console.log("--- Testing 'helloProcedure' ---");
// 3. Call the procedure's internal query handler
const result = await helloProcedure._def.query({
ctx: mockContext,
input: undefined, // No input required
path: 'hello', // The procedure's path
type: 'query' // It's a query procedure
});
// 4. Assert the result
console.log(`Expected: 'Hello tRPC'`);
console.log(`Actual: '${result}'`);
if (result === 'Hello tRPC') {
console.log("Test Passed: The procedure returned the correct message.");
} else {
console.log("Test Failed: Unexpected return value.");
}
}
runHelloTest();Isolating with Mock Context
The ctx object in tRPC procedures often contains important dependencies like user authentication status, database connections, or other services. To keep unit tests isolated, you must mock this context.
A mock context provides fake versions of these dependencies, allowing your procedure to run without interacting with real external systems.
// test-mock-context.ts
// Simulate a mock context for a procedure needing a user and database.
interface MockUser {
id: string;
name: string;
}
interface MockDB {
findUserById: (id: string) => Promise<MockUser | null>;
// ... other mock database methods
}
// Our mock database implementation
const mockDB: MockDB = {
findUserById: async (id: string) => {
console.log(`Mock DB: Finding user with ID: ${id}`);
if (id === 'user123') {
return { id: 'user123', name: 'Alice' };
}
return null;
},
};
// Our mock tRPC context factory
const createMockContext = (user?: MockUser) => ({
user: user || null,
db: mockDB,
// Add any other mocked services here
});
async function demonstrateMockContext() {
console.log("--- Demonstrating Mock Context ---");
const ctxWithUser = createMockContext({ id: 'user123', name: 'Alice' });
console.log("Context with user: ", JSON.stringify(ctxWithUser.user));
const ctxWithoutUser = createMockContext();
console.log("Context without user: ", JSON.stringify(ctxWithoutUser.user));
// You can even test mock DB calls
const foundUser = await ctxWithUser.db.findUserById('user123');
console.log("Found user via mock DB: ", JSON.stringify(foundUser));
}
demonstrateMockContext();Example: Querying User Profile
Let's combine what we've learned to test a query that fetches a user's profile. This procedure will depend on the ctx for user information and a mock database.
We'll create a mock context that simulates an authenticated user and assert the correct profile is returned.
// test-user-query.ts
import { initTRPC } from '@trpc/server';
// Define our mock DB and User types
interface MockUser { id: string; name: string; email: string; }
interface MockDB { getUserById: (id: string) => Promise<MockUser | null>; }
// Mock DB implementation
const mockDB: MockDB = {
getUserById: async (id: string) => {
if (id === 'alice123') {
return { id: 'alice123', name: 'Alice', email: 'alice@example.com' };
}
return null;
},
};
// Create a mock context for our tests
const createMockContext = (userId?: string) => ({
user: userId ? { id: userId, name: 'TestUser' } : null, // Simplified user obj
db: mockDB,
});
// Our tRPC setup
const t = initTRPC.create();
// The procedure we want to test
const getUserProfile = t.procedure
.query(async ({ ctx }) => {
if (!ctx.user) {
throw new Error('Unauthorized');
}
const user = await ctx.db.getUserById(ctx.user.id);
if (!user) {
throw new Error('User not found');
}
return { id: user.id, name: user.name, email: user.email };
});
async function runUserProfileTest() {
console.log("--- Testing 'getUserProfile' ---");
// Test Case 1: Authorized user
const authCtx = createMockContext('alice123');
try {
const result = await getUserProfile._def.query({
ctx: authCtx, input: undefined, path: 'getUserProfile', type: 'query'
});
console.log("Authorized User Test Result: ", JSON.stringify(result));
if (result.id === 'alice123' && result.name === 'Alice') {
console.log("Authorized User Test Passed!");
} else {
console.log("Authorized User Test Failed!");
}
} catch (error: any) {
console.log("Authorized User Test Failed with error: ", error.message);
}
// Test Case 2: Unauthorized user
const unauthCtx = createMockContext(undefined);
try {
await getUserProfile._def.query({
ctx: unauthCtx, input: undefined, path: 'getUserProfile', type: 'query'
});
console.log("Unauthorized User Test Failed: Should have thrown an error.");
} catch (error: any) {
console.log("Unauthorized User Test Result (Expected Error): ", error.message);
if (error.message === 'Unauthorized') {
console.log("Unauthorized User Test Passed!");
} else {
console.log("Unauthorized User Test Failed: Unexpected error message.");
}
}
}
runUserProfileTest();Validating Inputs with Zod
Many procedures accept input, often validated using Zod schemas. When unit testing, you need to provide valid and invalid inputs to ensure both the procedure's logic and its Zod validation work correctly.
The input parameter in the _def.query or _def.mutation handler is where you pass these test inputs.
// test-post-by-id.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
// Mock DB for posts
interface MockPost { id: string; title: string; content: string; }
interface MockDB { getPostById: (id: string) => Promise<MockPost | null>; }
const mockDB: MockDB = {
getPostById: async (id: string) => {
if (id === 'post1') {
return { id: 'post1', title: 'First Post', content: 'Lorem ipsum...' };
}
return null;
},
};
const t = initTRPC.create();
const mockContext = { db: mockDB }; // Simple context
// Procedure with Zod input validation
const getPostById = t.procedure
.input(z.object({
id: z.string().min(1, "ID cannot be empty") // Simplified validation for example
}))
.query(async ({ input, ctx }) => {
const post = await ctx.db.getPostById(input.id);
if (!post) {
throw new Error('Post not found');
}
return post;
});
async function runPostByIdTest() {
console.log("--- Testing 'getPostById' ---");
// Test Case 1: Valid input, post found
try {
const result = await getPostById._def.query({
ctx: mockContext,
input: { id: 'post1' }, // Valid input
path: 'getPostById',
type: 'query'
});
console.log("Valid Input (Found) Test Result: ", JSON.stringify(result));
if (result.id === 'post1') {
console.log("Valid Input (Found) Test Passed!");
} else {
console.log("Valid Input (Found) Test Failed!");
}
} catch (error: any) {
console.log("Valid Input (Found) Test Failed with error: ", error.message);
}
// Test Case 2: Valid input, post not found
try {
await getPostById._def.query({
ctx: mockContext,
input: { id: 'post2' }, // Valid format, but not in mock DB
path: 'getPostById',
type: 'query'
});
console.log("Valid Input (Not Found) Test Failed: Should have thrown error.");
} catch (error: any) {
console.log("Valid Input (Not Found) Test Result (Expected Error): ", error.message);
if (error.message === 'Post not found') {
console.log("Valid Input (Not Found) Test Passed!");
} else {
console.log("Valid Input (Not Found) Test Failed: Unexpected error.");
}
}
}
runPostByIdTest();Unit Testing Data Changes
Mutation procedures modify data. When testing them, you need to ensure they perform the correct actions and return the expected results. The key here is to mock any external systems (like a database) to verify that the mutation interacts with them as expected.
You'll often use mock functions (e.g., Jest mocks) to track calls to your mocked dependencies.
// test-create-user.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
// Mock DB for users
interface UserInput { name: string; email: string; }
interface MockDB { createUser: (data: UserInput) => Promise<{ id: string } & UserInput>; }
// --- Mocking 'jest.fn' for runnable code ---
// In a real environment, Jest would provide this. This is a simplified stand-in.
declare const jest: any; // Declare jest to avoid TS error in this isolated snippet
if (typeof jest === 'undefined') {
globalThis.jest = {
fn: (impl?: Function) => {
const mockFunc: any = (...args: any[]) => {
mockFunc.calls.push(args);
return impl ? impl(...args) : undefined;
};
mockFunc.calls = [];
mockFunc.mockClear = () => { mockFunc.calls = []; };
return mockFunc;
}
};
}
// --- End Mocking 'jest.fn' ---
// Implement a mock database with a 'mock' function for createUser
const mockCreateUser = jest.fn(async (data: UserInput) => {
// Simulate database ID generation
const newId = `user_${Math.random().toString(36).substr(2, 9)}`;
console.log(`Mock DB: Creating user with ID ${newId}`);
return { id: newId, ...data };
});
const mockDB: MockDB = {
createUser: mockCreateUser,
};
const t = initTRPC.create();
const mockContext = { db: mockDB };
// Our mutation procedure
const createUser = t.procedure
.input(z.object({
name: z.string().min(3, "Name too short"),
email: z.string().email("Invalid email format")
}))
.mutation(async ({ input, ctx }) => {
const newUser = await ctx.db.createUser(input);
return { success: true, userId: newUser.id };
});
async function runCreateUserTest() {
console.log("--- Testing 'createUser' Mutation ---");
// Reset mock before each test (conceptual)
mockCreateUser.mockClear();
// Test Case 1: Valid input
const validInput = { name: 'Bob', email: 'bob@example.com' };
try {
const result = await createUser._def.mutation({
ctx: mockContext,
input: validInput,
path: 'createUser',
type: 'mutation'
});
console.log("Valid Input Test Result: ", JSON.stringify(result));
// Assertions: check return value and if mock was called
if (result.success && result.userId) {
console.log("Valid Input Test Passed: Mutation returned success.");
} else {
console.log("Valid Input Test Failed: Unexpected return value.");
}
// In a real test, you'd assert mockCreateUser was called with validInput
console.log(`Mock createUser called: ${mockCreateUser.mock.calls.length > 0 ? 'Yes' : 'No'}`);
if (mockCreateUser.mock.calls.length > 0 && mockCreateUser.mock.calls[0][0].name === 'Bob') {
console.log("Mock createUser was called with correct data.");
}
} catch (error: any) {
console.log("Valid Input Test Failed with error: ", error.message);
}
// Test Case 2: Invalid input (email)
mockCreateUser.mockClear(); // Clear for next test
const invalidInput = { name: 'Charlie', email: 'invalid-email' };
try {
await createUser._def.mutation({
ctx: mockContext,
input: invalidInput,
path: 'createUser',
type: 'mutation'
});
console.log("Invalid Input Test Failed: Should have thrown Zod error.");
} catch (error: any) {
console.log("Invalid Input Test Result (Expected Zod Error): ", error.message);
if (error.message.includes('Invalid email format')) {
console.log("Invalid Input Test Passed: Zod validation caught the error.");
} else {
console.log("Invalid Input Test Failed: Unexpected error message.");
}
console.log(`Mock createUser called: ${mockCreateUser.mock.calls.length > 0 ? 'Yes' : 'No'}`);
if (mockCreateUser.mock.calls.length === 0) {
console.log("Mock createUser was NOT called (as expected due to Zod error).");
}
}
}
runCreateUserTest();Tips for Effective tRPC Tests
To maximize the value of your unit tests:
- Focus on Isolation: Mock all external dependencies (DB, API calls, auth).
- Test Edge Cases: Include valid, invalid, and boundary inputs.
- Descriptive Names: Use clear test names that explain what's being tested.
- Small & Focused: Each test should ideally verify one specific piece of logic.
- Assert Correctly: Check both return values and side effects (e.g., if a mock function was called).
Testing Your Knowledge
When unit testing a tRPC mutation procedure that interacts with a database, which of the following practices are crucial for maintaining test isolation and effectiveness?
Lesson Summary
Great job! You've learned the fundamentals of unit testing tRPC procedures.
- We covered why unit testing is important for reliability and speed.
- You saw how to call procedure handlers directly and mock the
ctxobject. - We practiced testing both query and mutation procedures, including those with Zod input validation.
Keep practicing these techniques to build robust and maintainable tRPC APIs!
Questions Fréquemment Posées
La leçon « Tests unitaires des procédures tRPC » est-elle gratuite ?
Oui — le texte complet de « Tests unitaires des procédures tRPC » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours tRPC End-to-End Type Safe APIs, passe à CoddyKit PRO. Le cours tRPC End-to-End Type Safe APIs comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « Tests unitaires des procédures tRPC » ?
Apprenez à écrire des tests unitaires isolés pour chaque procédure tRPC avec des cadriciels de test populaires. Tu pratiques tRPC End-to-End Type Safe APIs avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.
Dois-je avoir de l'expérience pour commencer tRPC End-to-End Type Safe APIs ?
Aucune expérience préalable n'est requise. tRPC End-to-End Type Safe APIs sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 1 sur 4.
Combien de temps prend la leçon « Tests unitaires des procédures tRPC » ?
La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.
Peux-tu écrire et exécuter du code dans cette leçon tRPC End-to-End Type Safe APIs ?
Oui. Chaque leçon tRPC End-to-End Type Safe APIs inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.
Toutes les leçons de ce cours
- Tests unitaires des procédures tRPC
- Tests d’intégration des routeurs tRPC
- Stratégies de tests de bout en bout
- Simulation du contexte et des dépendances dans les tests tRPC