tRPC API's met typeveiligheid van begin tot eind · Les

tRPC-procedures unittesten

Leer geïsoleerde unittests schrijven voor afzonderlijke tRPC-procedures met populaire testframeworks.

Les 1 van 411 stappen

tRPC-procedures unittesten is een gratis tRPC API's met typeveiligheid van begin tot eind-les op CoddyKit. Dit is les 1 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject tRPC API's met typeveiligheid van begin tot eind. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus tRPC API's met typeveiligheid van begin tot eind bevat in totaal 4 lessen.

Unittests voor tRPC-procedures

Welkom! In deze les gaan we dieper in op unittests voor tRPC. Bij unittests controleer je afzonderlijke, geïsoleerde onderdelen van je code om te verifiëren dat ze werken zoals verwacht.

Bij tRPC verwijst een "unit" vaak naar één procedure (query of mutatie). We leren hoe je deze procedures onafhankelijk kunt testen zonder dat er een volledige server hoeft te draaien.

Voordelen van proceduretests

Unittests van tRPC-procedures bieden verschillende belangrijke voordelen:

  • Betrouwbaarheid: ontdek bugs vroeg in het ontwikkelproces, voordat ze productie bereiken.
  • Snelheid: tests worden snel uitgevoerd en geven direct feedback.
  • Isolatie: lokaliseer problemen in specifieke procedures in plaats van in complexe integraties.
  • Typeveiligheid: zorgt ervoor dat de invoer en uitvoer van je procedure overeenkomen met je TypeScript-definities.

Essentiële hulpmiddelen voor tRPC-tests

Hoewel je verschillende hulpmiddelen kunt gebruiken, zijn Jest en Vitest populaire keuzes voor TypeScript-projecten als testuitvoerder. Voor unittests van een tRPC-backend richten we ons voornamelijk op TypeScript en de mogelijkheden van een testuitvoerder om beweringen te doen.

We simuleren rechtstreeks het aanroepen van de interne handler van een procedure voor het testen. Dat is het kernconcept, ongeacht welke testuitvoerder je kiest.

Een proceduretest ontleden

Om een tRPC-procedure met een unittest te testen, moet je:

  1. De procedure importeren.
  2. Een mockcontext maken (ctx).
  3. De interne handler van de procedure rechtstreeks aanroepen (bijvoorbeeld _def.query of _def.mutation).
  4. De geretourneerde waarde of eventuele neveneffecten controleren.

Laten we een eenvoudig voorbeeld bekijken:

// 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();

Isoleren met een mockcontext

Het object ctx in tRPC-procedures bevat vaak belangrijke afhankelijkheden, zoals de authenticatiestatus van de gebruiker, databaseverbindingen of andere diensten. Om unittests geïsoleerd te houden, moet je deze context nabootsen.

Een mockcontext biedt namaakversies van deze afhankelijkheden, zodat je procedure kan worden uitgevoerd zonder interactie met echte externe systemen.

// 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();

Voorbeeld: gebruikersprofiel opvragen

Laten we combineren wat we hebben geleerd om een query te testen die het profiel van een gebruiker ophaalt. Deze procedure is afhankelijk van ctx voor gebruikersinformatie en van een mockdatabase.

We maken een mockcontext die een geauthenticeerde gebruiker simuleert en controleren of het juiste profiel wordt geretourneerd.

// 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();

Invoer valideren met Zod

Veel procedures accepteren invoer die vaak wordt gevalideerd met Zod-schema's. Bij unittests moet je geldige en ongeldige invoer opgeven om te controleren of zowel de logica van de procedure als de Zod-validatie correct werken.

De parameter input in de handler _def.query of _def.mutation is de plek waar je deze testinvoer doorgeeft.

// 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();

Gegevenswijzigingen testen

Mutatieprocedures wijzigen gegevens. Bij het testen ervan moet je controleren of ze de juiste acties uitvoeren en de verwachte resultaten retourneren. De kern hiervan is dat je externe systemen, zoals een database, nabootst om te controleren of de mutatie er volgens verwachting mee communiceert.

Vaak gebruik je mockfuncties (bijvoorbeeld Jest-mocks) om aanroepen naar je nagebootste afhankelijkheden bij te houden.

// 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 voor effectieve tRPC-tests

Zo haal je maximaal voordeel uit je unittests:

  • Richt je op isolatie: boots alle externe afhankelijkheden na (DB, API-aanroepen, authenticatie).
  • Test randgevallen: neem geldige, ongeldige en grenswaarden als invoer op.
  • Beschrijvende namen: gebruik duidelijke testnamen die uitleggen wat er wordt getest.
  • Klein en gericht: elke test moet idealiter één specifiek stukje logica controleren.
  • Controleer correct: controleer zowel geretourneerde waarden als neveneffecten (bijvoorbeeld of een mockfunctie is aangeroepen).

Je kennis testen

Welke van de volgende werkwijzen zijn cruciaal om de isolatie en effectiviteit van tests te behouden wanneer je een tRPC-mutatieprocedure test die met een database werkt?

Samenvatting van de les

Goed gedaan! Je hebt de basisprincipes van unittests voor tRPC-procedures geleerd.

  • We hebben besproken waarom unittests belangrijk zijn voor betrouwbaarheid en snelheid.
  • Je hebt gezien hoe je procedurehandlers rechtstreeks aanroept en het object ctx nabootst.
  • We hebben geoefend met het testen van zowel query- als mutatieprocedures, inclusief procedures met Zod-invoervalidatie.

Blijf deze technieken oefenen om robuuste en onderhoudbare tRPC-API's te bouwen!

Gratis beginnen

Leer tRPC API's met typeveiligheid van begin tot eind met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
10
Lessen
40

Veelgestelde vragen

Is de les “tRPC-procedures unittesten” gratis?

Ja — je kunt hier op het web alle 3 lessen van het leerpad tRPC API's met typeveiligheid van begin tot eind, waaronder “tRPC-procedures unittesten”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus tRPC API's met typeveiligheid van begin tot eind bevat in totaal 4 lessen.

Wat leer ik in “tRPC-procedures unittesten”?

Leer geïsoleerde unittests schrijven voor afzonderlijke tRPC-procedures met populaire testframeworks. Je oefent met tRPC API's met typeveiligheid van begin tot eind door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met tRPC API's met typeveiligheid van begin tot eind te beginnen?

Ervaring vooraf is niet nodig. tRPC API's met typeveiligheid van begin tot eind op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 1 van 4.

Hoe lang duurt de les “tRPC-procedures unittesten”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over tRPC API's met typeveiligheid van begin tot eind?

Ja. Elke les over tRPC API's met typeveiligheid van begin tot eind bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. tRPC-procedures unittesten
  2. tRPC-routers integratietesten
  3. Strategieën voor end-to-endtests
  4. Context en dependencies mocken in tRPC-tests
← Terug naar tRPC API's met typeveiligheid van begin tot eind