0Pricing
tRPC End-to-End Type Safe APIs · Pelajaran

Pengujian Unit Prosedur tRPC

Pelajari cara menulis pengujian unit terisolasi untuk setiap prosedur tRPC menggunakan kerangka kerja pengujian populer.

Pengujian Unit Prosedur tRPC adalah pelajaran tRPC End-to-End Type Safe APIs gratis di CoddyKit. Ini adalah pelajaran 1 dari 4. Kamu bisa membaca pelajaran lengkapnya di bawah secara gratis — lalu praktikkan langsung di browser dengan editor kode bawaan dan tutor AI 24/7. Ini adalah bagian dari jalur belajar tRPC End-to-End Type Safe APIs, dan progresmu tersinkronisasi di web dan aplikasi CoddyKit. Kursus tRPC End-to-End Type Safe APIs mencakup 4 pelajaran total.

Bagian dari pelajaran ini belum diterjemahkan dan ditampilkan dalam bahasa Inggris.

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:

  1. Import the procedure.
  2. Create a mock context (ctx).
  3. Call the procedure's internal handler directly (e.g., _def.query or _def.mutation).
  4. 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 ctx object.
  • 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!

Pertanyaan yang Sering Diajukan

Apakah pelajaran “Pengujian Unit Prosedur tRPC” gratis?

Ya — teks lengkap “Pengujian Unit Prosedur tRPC” gratis dibaca di sini di web. Untuk praktiknya secara interaktif (editor kode bawaan dan tutor AI 24/7) dan buka sisa kursus tRPC End-to-End Type Safe APIs, upgrade ke CoddyKit PRO. Kursus tRPC End-to-End Type Safe APIs mencakup 4 pelajaran total.

Apa yang akan aku pelajari di “Pengujian Unit Prosedur tRPC”?

Pelajari cara menulis pengujian unit terisolasi untuk setiap prosedur tRPC menggunakan kerangka kerja pengujian populer. Kamu berlatih tRPC End-to-End Type Safe APIs dengan kode praktik yang langsung kamu jalankan di browser, dan tutor AI 24/7 menjawab pertanyaanmu saat kamu mengerjakan pelajaran ini.

Apakah aku perlu pengalaman untuk memulai tRPC End-to-End Type Safe APIs?

Tidak diperlukan pengalaman sebelumnya. tRPC End-to-End Type Safe APIs di CoddyKit dirancang untuk pemula hingga pelajar tingkat lanjut, jadi kamu bisa memulai di sini atau dari awal dan belajar sesuai kecepatan kamu sendiri. Ini adalah pelajaran 1 dari 4.

Berapa lama pelajaran “Pengujian Unit Prosedur tRPC” memakan waktu?

Sebagian besar pelajaran CoddyKit memakan waktu sekitar 5–10 menit. Setiap pelajaran ringkas dan interaktif, jadi kamu membuat kemajuan stabil dan melanjutkan dari tempat kamu tinggalkan di web dan aplikasi.

Bisakah aku menulis dan menjalankan kode dalam pelajaran tRPC End-to-End Type Safe APIs ini?

Ya. Setiap pelajaran tRPC End-to-End Type Safe APIs menyertakan editor kode bawaan, jadi kamu menulis dan menjalankan kode nyata langsung di browser dan mendapatkan umpan balik AI instan — tidak diperlukan penyiapan lokal.

Semua pelajaran dalam kursus ini

  1. Pengujian Unit Prosedur tRPC
  2. Pengujian Integrasi Router tRPC
  3. Strategi Pengujian Menyeluruh
  4. Mocking Context dan Dependensi dalam Pengujian tRPC
← Kembali ke tRPC End-to-End Type Safe APIs