Background on Mocking
The goal of unit testing is to test code as a modular unit/component/function, and not be concerned with the behaviour of external dependencies such as external libraries, databases or other parts of your own app.
Mocking is a solution to ensure the full isolation of the unit to be tested. The idea is to replace the inner workings of external dependencies with expected repeatable outputs, so that you have a clean slate upon which to run your unit tests. Authentication is therefore a prime candidate for this approach. We use Clerk here as the
Set up Clerk
Firstly install and setup Clerk as per the official Next.js docs.
Set up Vitest
Install using your favourite package manager.
pnpm add -D vitest @vitejs/plugin-react
Then set up a vitest.config.ts
file in the root of your project.
/// <reference types="vitest" />
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: "jsdom",
mockReset: true,
},
});
Create Component you wish to Unit test
// src/components/ClerkComponent.tsx
import { SignOutButton, SignInButton, useUser } from "@clerk/nextjs";
const ClerkComponent = () => {
const { user, isSignedIn, isLoaded } = useUser();
return (
<div>
{isSignedIn && (
<SignOutButton>
<button>Sign out</button>
</SignOutButton>
)}
{!isSignedIn && (
<SignInButton>
<button>Sign in</button>
</SignInButton>
)}
</div>
);
};
export default ClerkComponent;
Notice that we have an external dependency on the @clerk/nextjs
library. We will need to mock this dependency in order to test our component in isolation.
Setting up Vitest Mock
We will use the vi.mock
function to mock this external dependency. The first argument to vi.mock
is the name of the module you wish to mock, and the second argument is a function that returns the mocked module. Note here that we only need to mock the methods that we are importing from the @clerk/nextjs
library - namely SignInButton
, SignOutButton
and useUser
.
Also notice that for this setup, the isSignedIn
property is set to false
, meaning that the SignOutButton
will not be rendered, and the SignInButton
with the inner text "Sign in"
will be rendered.
import { vi } from "vitest";
vi.mock("@clerk/nextjs", () => {
// Create an mockedFunctions object to match the functions we are importing from the @nextjs/clerk package in the ClerkComponent component.
const mockedFunctions = {
SignInButton: ({ children }: SignInOutButtonProps) => <div>{children}</div>,
SignOutButton: ({ children }: SignInOutButtonProps) => (
<div>{children}</div>
),
useUser: () => ({
isSignedIn: true,
user: {
id: "user_2NNEqL2nrIRdJ194ndJqAHwEfxC",
fullName: "Charles Harris",
},
}),
};
return mockedFunctions;
});
Writing the unit test
Now we can write our unit test. Even if you have only useed Jest thus far, the syntax should be familiar to you:
// test/ClerkComponent.test.tsx
import { it, render, screen } from "vitest";
import { ClerkComponent } from "..src/components/ClerkComponent";
it("Test Clerk signin", () => {
render(<ClerkComponent />);
screen.getByText("Sign in");
});
Assuming the mock had been set up with iSignedIn
set to true
, the test would fail, as the SignOutButton
would be rendered instead of the SignInButton
, and we would expect the text to be "Sign out"
.
Conclusion
With this setup, we can easily integrate unit test authed components in a predictable way, without having to make any network requests to Clerk. The code for this post can be found here.