- Published on
How to implement API testing for tRPC APIs with Jest
- Author
- Name
- Karuppusamy
- Headline
- Developer
Introduction
In this post, I am going to show how to implement API testing for tRPC APIs with Jest.
What is tRPC?
tRPC is a typescript first framework for building end-to-end typesafe APIs with typescript. It is an alternative to REST and GraphQL APIs. Learn more about tRPC here.
Initial setup
First, we need an application with a tRPC server. For that, I am going to use the t3 starter template. You can also use your existing project or any other tRPC project.
npm create t3-app@latest
For more information about the t3 starter template visit here
Initial setup for Jest
Let's set up Jest for our project. Run the following command to install Jest and other required packages. Learn more about Jest here
npm install --save-dev jest @jest/globals ts-jest
Now, add scripts to run Jest in the package.json
file.
{
"scripts": {
"test": "jest"
}
}
Configure Jest to work with typescript
By default, Jest can run without any config files, but it will not compile .ts files. To make it transpile typescript with ts-jest, we will need to create a configuration file that will tell Jest to use a ts-jest transformer. Learn more about ts-jest here
Generate a basic configuration file for Jest using the following command.
npx ts-jest config:init
This will create a jest.config.js
file in the root of your project. You can modify this file according to your needs.
To support typescript for .spec.ts
and .test.ts
files, update tsconfig.json
to include the following configuration.
{
"include": ["**/*.spec.ts", "**/*.test.ts"]
}
Create helper functions for API testing
Before writing tests, we need to create helper functions to make API requests.
Create a new file api.ts
in the tests
folder and add the following code.
import { createCaller } from "@/server/api/root";
import { db } from "@/server/db";
import type { Session } from "next-auth";
type User = Session["user"];
export function getAPIForTest(user?: User) {
const ctx = createTRPCContextForJest(user);
return createCaller(ctx);
}
export function createTRPCContextForJest(user?: User) {
if (!user) {
return {
db,
headers: new Headers(),
session: null,
};
}
return {
db,
headers: new Headers(),
session: {
user,
// for testing purposes, we can set the session to expire in the future.
expires: new Date(new Date().getTime() + 1000 * 60 * 60).toISOString(),
},
};
}
Writing tests
Let's write a test for the post
API. Create a new file post.spec.ts
in the tests
folder and add the following code.
import { db } from "@/server/db";
import { beforeAll, describe, expect, it } from "@jest/globals";
import type { User } from "@prisma/client";
import { TRPCError } from "@trpc/server";
import { randomInt } from "crypto";
import { getAPIForTest } from "./api";
describe("post API", () => {
let user: User | null = null;
beforeAll(async () => {
user = await db.user.findFirst();
});
const randomString = `This is a random string ${randomInt(100)}`;
it("should return hello world", async () => {
expect(user).not.toBeNull();
const api = getAPIForTest({
id: user!.id,
email: user!.email,
});
const greeting = await api.post.hello({ text: "world" });
expect(greeting).toStrictEqual({ greeting: "Hello world" });
});
it("should be able to add a post", async () => {
expect(user).not.toBeNull();
const api = getAPIForTest({
id: user!.id,
email: user!.email,
});
const res = await api.post.create({
name: randomString,
});
expect(res.id).toBeDefined();
expect(res.name).toBe(randomString);
expect(res.createdAt).toBeDefined();
expect(res.updatedAt).toBeDefined();
expect(res.createdById).toBe(user!.id);
});
it("should return the latest post", async () => {
expect(user).not.toBeNull();
const api = getAPIForTest({
id: user!.id,
email: user!.email,
});
const res = await api.post.getLatest();
expect(res?.name).toBe(randomString);
});
it("should be able to get the secret message", async () => {
expect(user).not.toBeNull();
const api = getAPIForTest({
id: user!.id,
email: user!.email,
});
const res = await api.post.getSecretMessage();
expect(res).toBe("you can now see this secret message!");
});
it("should not be able to get the secret message if not logged in", async () => {
const api = getAPIForTest();
expect.assertions(2);
try {
await api.post.getSecretMessage();
} catch (error) {
expect(error).toBeInstanceOf(TRPCError);
expect((error as TRPCError).code).toBe("UNAUTHORIZED");
}
});
});
Running tests
You can run the tests using the following command.
npm run test
Common errors
While running the tests, you may encounter some errors. Let's see common errors and how to fix them.
Cannot find module '@/server/api/root' from 'src/tests/api.ts'
This error occurs when Jest is not able to resolve the module. To fix this error, you need to update the moduleNameMapper
in the jest.config.js
file to map the module to the actual path.
module.exports = {
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
},
};
SyntaxError: Cannot use import statement outside a module.
This error occurs when Jest is not able to transpile the package that is causing the error or Jest is not able to find the JavaScript files.
To fix this error, update jest.config.js
file to include transform for javascript files and enable isolatedModules
option. Then update the transformIgnorePatterns
to include the package that is causing the error.
module.exports = {
transform: {
"^.+\\.[tj]s$": [
"ts-jest",
{
isolatedModules: true,
},
],
},
transformIgnorePatterns: ["node_modules\\(?!(superjson)\\)"],
// transformIgnorePatterns: ["node_modules\\.pnpm\\(?!(superjson)\\)"], # For pnpm
};
TypeError: (0 , react_1.cache) is not a function
To fix this error, we need to mock the missing imports. To do that,
- Update the
setupFilesAfterEnv
injest.config.js
file to includejest.setup.ts
setup.
module.exports = {
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
};
- Create a new file
jest.setup.ts
in the root of your project. I have added mocks for react cache and next-auth providers. You can add more mocks if needed.
import { jest } from "@jest/globals";
// mock react cache
jest.mock("react", () => {
const react = jest.requireActual("react");
return {
__esModule: true,
...react,
cache: jest.fn().mockImplementation((val) => {
if (typeof val === "function") {
return val();
}
return val;
}),
};
});
jest.mock("next-auth/providers/google", () => ({
__esModule: true,
default: jest.fn().mockReturnValue({
id: "google",
name: "Google",
type: "oidc",
issuer: "https://accounts.google.com",
}),
}));
// mock next-auth
jest.mock("next-auth", () => ({
__esModule: true,
default: jest.fn().mockReturnValue({
handlers: {},
signIn: jest.fn(),
signOut: jest.fn(),
auth: jest.fn(),
}),
}));