DEV Community

Roland Chelwing
Roland Chelwing

Posted on

Mocking tRPC Routes with Type Safety in TypeScript

Testing in modern web development is paramount, and mocking plays an integral role, enabling us to emulate real-world scenarios. Yet, when working with React Server Components, challenges arise.

Tools like Cypress or Playwright can’t intercept server calls, making testing in a Next.js environment more intricate.

For developers navigating this space, TypeScript and tRPC become invaluable allies, offering a solution to ensure accurate and consistent mocks. This article delves into leveraging these tools to create robust mocks, particularly when traditional interception methods fall short.

The problem at hand

With the ever-evolving landscape of web development, the React community was introduced to React Server Components. Server components offered a breath of fresh air, providing a unique way to fetch and render data directly on the server side. But as with all innovations, new challenges emerged.

The primary hiccup developers encountered was testing. Traditional methods, such as intercepting network requests using tools like Cypress or Playwright, were no longer effective. Why? Because React Server Components, combined with frameworks like Next.js, don’t make traditional AJAX calls. Instead, they fetch data seamlessly on the server side, rendering it directly. It’s efficient, yes, but a bit of a curveball for those of us in the testing realm.

Surrounded by bugs and errors.

You might be thinking, “So, what’s the big deal? Can’t we mock the data?” Well, yes and no. While mocking is indeed the solution, the intricacies lie in how to mock effectively. We need a consistent and type-safe way to ensure that our mocked data aligns with real-world data structures, especially when our applications evolve and change.

The Solution: TypeScript and tRPC

As developers, we’re no strangers to problem-solving. When one door closes, we hunt down another one or, occasionally, build a new door ourselves. In our quest for a robust solution, the synergy of TypeScript and tRPC seemed promising. Let’s unpack this approach step by step.

TypeScript to the rescue.

1. Embracing TypeScript

TypeScript offers static typing, which is an absolute blessing for large-scale applications. Not only does it catch potential bugs during development, but it also ensures that our data remains consistent across the board.

With TypeScript, we can define our data shapes, ensuring that the mocked data is consistent with our actual data structures. If a new developer comes on board and isn’t aware of the mocking intricacies, TypeScript will throw a compile-time error if there’s any inconsistency.

2. Harnessing the Power of tRPC

tRPC bridges the gap between the API layer and frontend, allowing type-safe end-to-end requests without the need for manual type definitions or API schema syncing.

By utilizing tRPC in conjunction with TypeScript, we can:

  • Extract the expected output types of our tRPC routes.

  • Use these types to define the shape of our mock data.

This synergy ensures that our mock data always aligns with the expected output types. If our tRPC route changes, TypeScript will alert us to any inconsistencies in our mock data.

3. Automating Mock Creation

We can create a utility function to minimize human error and the repetitive task of manually updating mocks. This function takes the original tRPC route as input and returns a mock version, ensuring type consistency along the way. This dynamic, automated process greatly reduces the likelihood of outdated or misaligned mock data, keeping our tests relevant and trustworthy.

With these tools at our disposal, the challenge of effectively testing React Server Components in a Next.js environment becomes far less daunting. Our testing suite can easily adapt as our application evolves, ensuring we always deliver reliable, high-quality software.

To truly grasp the power and simplicity of this approach, let’s dive into a hands-on example in the next section.

A Practical Walkthrough

This section aims to provide a tangible example, simplifying the theoretical into actionable steps. By the end, you should have a clear path on how to implement the described solution in your Next.js project.

Step-by-step solution.

1. Setting Up the Original tRPC Route

First, let’s define our tRPC route. This route fetches some imaginary data, maybe a list of articles:

    // routes/items.ts
    import { z } from "zod";
    import { router, publicProcedure } from "../trpc-setup";

    export const items = router({
      get: publicProcedure
        .input(z.void())
        .output(
          z.object({
            items: z.array(
              z.object({
                id: z.string(),
                title: z.string(),
                modified_at: z.date().transform((date) => date.toDateString()),
                link: z.string().url(),
              }),
            ),
            total: z.number(),
          })
        )
        .query(async ({ ctx }) => {
          const { total, entries } = await fetchDataFromSomewhere(ctx);
          return {
            items: entries,
            total,
          };
        }),
    });
Enter fullscreen mode Exit fullscreen mode

2. Harnessing TypeScript to Automate Mock Return Type Consistency

We’ve created a utility function that ensures our mock data’s return type is consistent with our original route:

    // utils/mock-utils.ts
    type OutputType<T> = T extends { _def: { _output_out: infer O } } ? O : never;

    export function createMockProcedure<OriginalProcedure>(mockData: OutputType<OriginalProcedure>) {
      return () => mockData;
    }
Enter fullscreen mode Exit fullscreen mode

3. Using the Utility Function for Mocks

Leveraging our utility function, we can quickly craft mock routes that remain type-consistent:

    // mocks/items.ts
    import { createMockProcedure } from "@/utils/mock-utils";
    import { router, publicProcedure } from "../trpc-setup";
    import type { items } from "../routes/items";

    export const mockItems = router({
      get: publicProcedure.query(
        createMockProcedure<typeof items.get>({
          items: [
            {
              id: "1",
              title: "Test data lol",
              modified_at: new Date().toDateString(),
              link: "https://url.com",
            },
          ],
          total: 1,
        }),
      )
    });
Enter fullscreen mode Exit fullscreen mode

4. Switching Between Mock and Original Routes

To use mocks during testing, set up a conditional based on the process.env variable:

    import { router } from "./trpc-setup";
    import { items } from "./routes/items";
    import { mockItems } from "./mocks/items";

    export const appRouter = process.env["TEST"]
      ? router({ items: mockItems })
      : router({ items });
Enter fullscreen mode Exit fullscreen mode

We've made a robust testing environment by combining the power of TypeScript and tRPC. This ensures that your mock data remains consistent with your tRPC routes, regardless of your application’s changes. The potential pitfalls of type discrepancies in testing are now a thing of the past.

Just sit back and relax.

Wrapping Up

This step-by-step guide demonstrated how to ensure your mock data remains in sync with your tRPC routes. You can create a resilient and efficient testing suite by leveraging TypeScript’s capabilities in a Next.js environment. It’s all about writing less, doing more, and catching potential errors early. Happy testing!

Have a different solution or an enhancement? Please let me know.

Top comments (0)