Enhedstest af tRPC-procedurer
Lær at skrive isolerede enhedstests af individuelle tRPC-procedurer ved hjælp af populære testframeworks.
Enhedstest af tRPC-procedurer er en gratis Typesikre API'er fra frontend til backend med tRPC-lektion på CoddyKit. Dette er lektion 1 af 4. Du kan læse alle 3 lektioner i dette læringsspor gratis i deres fulde længde — derefter låser CoddyKit PRO alle lektioner op samt praktiske øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Den er en del af læringsforløbet i Typesikre API'er fra frontend til backend med tRPC, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Typesikre API'er fra frontend til backend med tRPC-kurset indeholder 4 lektioner i alt.
Enhedstest af tRPC-procedurer
Velkommen! I denne lektion går vi i dybden med enhedstest af tRPC. Enhedstest fokuserer på at kontrollere individuelle, isolerede dele af din kode og sikre, at de fungerer som forventet.
I tRPC henviser en "enhed" ofte til en enkelt procedure (forespørgsel eller ændring). Vi lærer, hvordan du tester disse procedurer uafhængigt uden at skulle have en hel server kørende.
Fordele ved test af procedurer
Enhedstest af tRPC-procedurer har flere vigtige fordele:
- Pålidelighed: Opdag fejl tidligt i udviklingen, før de når produktionen.
- Hastighed: Testene kører hurtigt og giver øjeblikkelig feedback.
- Isolation: Find problemer i bestemte procedurer i stedet for i komplekse integrationer.
- Typesikkerhed: Sikrer, at procedurens input og output stemmer overens med dine TypeScript-definitioner.
Vigtige værktøjer til tRPC-tests
Du kan bruge forskellige værktøjer, men populære valg til TypeScript-projekter omfatter Jest eller Vitest som testrammeværk. Til enhedstest af tRPC-backend fokuserer vi primært på TypeScript og et testrammeværks funktioner til forventninger.
Vi simulerer, hvordan du kalder en procedures interne handler direkte til test, hvilket er det grundlæggende koncept uanset dit valg af testrammeværk.
Gennemgang af en proceduretest
For at enhedsteste en tRPC-procedure skal du:
- Importere proceduren.
- Oprette en mock-kontekst (
ctx). - Kalde procedurens interne handler direkte (f.eks.
_def.queryeller_def.mutation). - Kontrollere den returnerede værdi eller eventuelle sideeffekter.
Lad os se på et grundlæggende eksempel:
// 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();Isolation med mock-kontekst
Objektet ctx i tRPC-procedurer indeholder ofte vigtige afhængigheder som brugerens godkendelsesstatus, databaseforbindelser eller andre tjenester. For at holde enhedstest isolerede skal du mocke denne kontekst.
En mock-kontekst leverer falske versioner af disse afhængigheder, så din procedure kan køre uden at interagere med rigtige eksterne systemer.
// 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();Eksempel: Hentning af brugerprofil
Lad os samle det, vi har lært, og teste en forespørgsel, der henter en brugers profil. Denne procedure afhænger af ctx for at få brugeroplysninger og af en mock-database.
Vi opretter en mock-kontekst, der simulerer en godkendt bruger, og kontrollerer, at den korrekte profil returneres.
// 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();Validering af input med Zod
Mange procedurer accepterer input, som ofte valideres ved hjælp af Zod-skemaer. Ved enhedstest skal du angive både gyldigt og ugyldigt input for at sikre, at både procedurens logik og dens Zod-validering fungerer korrekt.
Parameteren input i handleren _def.query eller _def.mutation er stedet, hvor du sender disse testinput.
// 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();Enhedstest af dataændringer
Ændringsprocedurer ændrer data. Når du tester dem, skal du sikre, at de udfører de korrekte handlinger og returnerer de forventede resultater. Det afgørende er at mocke eksterne systemer, f.eks. en database, så du kan kontrollere, at ændringen interagerer med dem som forventet.
Du bruger ofte mock-funktioner, f.eks. Jest-mocks, til at spore kald til dine mockede afhængigheder.
// 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();Tip til effektive tRPC-tests
Sådan får du mest muligt ud af dine enhedstest:
- Fokusér på isolation: Mock alle eksterne afhængigheder (DB, API-kald, godkendelse).
- Test grænsetilfælde: Medtag gyldigt, ugyldigt og grænseoverskridende input.
- Beskrivende navne: Brug tydelige testnavne, der forklarer, hvad der testes.
- Små og fokuserede: Hver test bør helst kontrollere ét bestemt stykke logik.
- Kontrollér korrekt: Kontrollér både returværdier og sideeffekter, f.eks. om en mock-funktion blev kaldt.
Test din viden
Når du enhedstester en tRPC-ændringsprocedure, der interagerer med en database, hvilke af følgende fremgangsmåder er så afgørende for at bevare testenes isolation og effektivitet?
Opsummering af lektionen
Godt gået! Du har lært det grundlæggende i enhedstest af tRPC-procedurer.
- Vi gennemgik, hvorfor enhedstest er vigtigt for pålidelighed og hastighed.
- Du så, hvordan du kalder procedurehandlere direkte og mocker
ctx-objektet. - Vi øvede os i at teste både forespørgsels- og ændringsprocedurer, herunder dem med Zod-inputvalidering.
Bliv ved med at øve disse teknikker for at bygge robuste og vedligeholdelsesvenlige tRPC-API'er!
Lær Typesikre API'er fra frontend til backend med tRPC med en AI-underviser — gratis
Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.
- Kurser
- 10
- Lektioner
- 40
Ofte stillede spørgsmål
Er lektionen “Enhedstest af tRPC-procedurer” gratis?
Ja — alle 3 lektioner i læringssporet Typesikre API'er fra frontend til backend med tRPC, inklusive “Enhedstest af tRPC-procedurer”, kan læses gratis i deres fulde længde her på webstedet. Derefter låser CoddyKit PRO alle lektioner op samt interaktive øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Typesikre API'er fra frontend til backend med tRPC-kurset indeholder 4 lektioner i alt.
Hvad lærer jeg i “Enhedstest af tRPC-procedurer”?
Lær at skrive isolerede enhedstests af individuelle tRPC-procedurer ved hjælp af populære testframeworks. Du øver dig i Typesikre API'er fra frontend til backend med tRPC med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.
Skal jeg have erfaring for at begynde på Typesikre API'er fra frontend til backend med tRPC?
Der kræves ingen tidligere erfaring. Typesikre API'er fra frontend til backend med tRPC på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 1 af 4.
Hvor lang tid tager lektionen “Enhedstest af tRPC-procedurer”?
De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.
Kan jeg skrive og køre kode i denne Typesikre API'er fra frontend til backend med tRPC-lektion?
Ja. Alle Typesikre API'er fra frontend til backend med tRPC-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.
Alle lektioner i dette kursus
- Enhedstest af tRPC-procedurer
- Integrationstest af tRPC-routere
- Strategier for end-to-end-test
- Mocking af context og dependencies i tRPC-tests