Jest Unit Testing - Part 1
While unit-testing my app, I got stuck while trying to mock a few external libraries. I list them here in case someone else runs into similar issues. In this first post of the series, I'll start with testing the auth middleware by learning how to mock next-auth and Prisma, and testing high-order functions.
What we are testing
In Platform template's auth.ts
, we need to mock getServerSession
from Next-auth, as well as Prisma's methods for interacting with the database. Furthermore, we have functions, such as withSiteAuth
, which are examples of high-order functions. They are called this because they return a new function designed to wrap around an action, adding things like authentication and authorization checks.
Here is the full function, from lib/auth/auth.ts
export function withSiteAuth(action: any) {
return async (
formData: FormData | null,
siteId: string,
key: string | null,
) => {
const session = await getSession();
if (!session) {
return {
error: "Not authenticated",
};
}
const site = await prisma.site.findUnique({
where: {
id: siteId,
},
});
if (!site || site.userId !== session.user.id) {
return {
error: "Not authorized",
};
}
return action(formData, site, key);
};
}
In the case of withSiteAuth
, the user must be both authenticated with next-auth, and authorized to perform an action on this particular site. In order to be authorized, the user id associated with this site must match the current user's id.
How to mock next-auth
To mock getServerSession
from next-auth
, add this on top of the file:
jest.mock("next-auth", () => ({
__esModule: true,
getServerSession: jest.fn(),
}));
You'll also need a mock user such as this one:
const mockUser: any = {
user: {
id: "userId",
name: "John Doe",
username: "johndoe",
email: "johndoe@example.com",
image: "https://example.com/johndoe.jpg"
}
}
How to mock Prisma
In order to mock Prisma, you first need to install jest-mock-extended
as a dev dependency. Then, create a file in your lib folder named singleton.ts:
import { PrismaClient } from '@prisma/client'
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'
import prisma from '@/lib/prisma'
jest.mock('@/lib/prisma', () => ({
__esModule: true,
default: mockDeep<PrismaClient>(),
}))
beforeEach(() => {
mockReset(prismaMock)
})
export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>
Now, in your jest.config file, add this filename to your setupFilesAfterEnv: setupFilesAfterEnv: ['<rootDir>/jest.setup.ts', '<rootDir>/lib/singleton.ts'],
Now you just need to import prismMock into your test file:
import { prismaMock } from '@/lib/singleton';
and you are all set to use it. For more on testing with Prisma, see the official docs: https://www.prisma.io/docs/orm/prisma-client/testing/unit-testing
Putting it all together
auth.test.ts (happy path)
it('should return the result of the action function when the user is authenticated and authorized for the given site', async () => {
// mock getServerSession to return mockUser we created above
const getServerSessionMock = require('next-auth').getServerSession;
getServerSessionMock.mockResolvedValue(mockUser);
// action is the function that gets passed into the high-order function
const action = jest.fn();
// function params
const formData = new FormData();
const siteId = "siteId";
const key = "key";
// a mock site with userId that matches our mock user's id
const site = {
id: "siteId",
userId: "userId",
name: "Site Name",
description: "Site Description",
createdAt: new Date(),
updatedAt: new Date(),
};
// mock prisma's call to find the site in the database
prismaMock.site.findUnique.mockResolvedValue(site as Site);
// finally, here is the function call
await withSiteAuth(action)(formData, siteId, key);
// Ensure the action was called with the correct arguments
expect(action).toHaveBeenCalledWith(formData, site, key);
// Ensure getSession was called
expect(getServerSessionMock).toHaveBeenCalled();
expect(prismaMock.site.findUnique).toHaveBeenCalledWith({
where: { id: siteId },
});
});
For an unauthenticated user scenario, simply return null from the user session:
it('should return an error object with message "Not authenticated" when the user is not authenticated', async () => {
const getServerSessionMock = require('next-auth').getServerSession;
// getServerSession returns null
getServerSessionMock.mockResolvedValue(null);
const action = jest.fn();
const formData = new FormData();
const siteId = "siteId";
const key = "key";
// function call
const result = await withSiteAuth(action)(formData, siteId, key);
expect(result).toEqual({
error: "Not authenticated",
});
expect(getServerSessionMock).toHaveBeenCalled();
});
And for unauthorized, there are multiple scenarios:
if (!site || site.userId !== session.user.id)
I'm going to show the one where user id and site user id do not match.
it('should return an error object with message "Not authorized" when the user is authenticated but not authorized for the given site', async () => {
const getServerSessionMock = require('next-auth').getServerSession;
getServerSessionMock.mockResolvedValue(mockUser);
const site = {
id: "siteId",
userId: "anotherUserId", //differs from our mock user id
name: "Site Name",
description: "Site Description",
createdAt: new Date(),
updatedAt: new Date(),
};
const action = jest.fn();
const formData = new FormData();
const siteId = "siteId";
const key = "key";
prismaMock.site.findUnique.mockResolvedValueOnce(site as Site);
const result = await withSiteAuth(action)(formData, siteId, key);
expect(result).toEqual({
error: "Not authorized",
});
expect(getServerSessionMock).toHaveBeenCalled();
expect(prismaMock.site.findUnique).toHaveBeenCalled();
});
This pattern can be used for your other tests of auth high-order functions.
I hope you found this helpful. Stay tuned for part 2, where I'll write about testing Next.js route handlers.
Happy Coding!
Subscribe to my newsletter
Read articles from Nada Pivcevic directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by