Testing with Cypress
Cypress is a Javascript End to End Testing framework that is popular, easy to use, and very powerful. This document will go over the setup you need to write authenticated tests with Clerk. This guide will assume you're somewhat familiar with Clerk and Cypress.
If you would like to see a full example of a Cypress project with Clerk, check out this repo.
Since Cypress does not allow third-party cookies by default, you might be interested in the experimental Cookieless Dev mode.
Cypress commands
Easiest way to test with Clerk is to create sign in and sign out helper thats you can re-use in all of your tests.
Sign out helper
This helper is simple, and just clears all cookies, which effectively puts the browser back into a "signed out" state
commands.jsCypress.Commands.add(`signOut`, () => { cy.log(`sign out by clearing all cookies.`); cy.clearCookies({ domain: null }); });
Sign in helper
This helper is dependent on your authentication strategy, and just signs in an existing user. The following helper uses an email/pw strategy to sign in the user. If you're using phone codes, you should use Test emails and phones.
The helper will read from the Cypress environment variables. To define the email and password, a cypress.env.json
can be created.
Be careful not to call this method once per test. You will get rate-limited. Instead, sign in once, then re-use the same session for multiple tests. See below for an example.
commands.jsCypress.Commands.add(`signIn`, () => { cy.log(`Signing in.`); cy.visit(`/`); cy.window() .should((window) => { expect(window).to.not.have.property(`Clerk`, undefined); expect(window.Clerk.isReady()).to.eq(true); }) .then(async (window) => { await cy.clearCookies({ domain: window.location.domain }); const res = await window.Clerk.client.signIn.create({ identifier: Cypress.env(`test_email`), password: Cypress.env(`test_password`), }); await window.Clerk.setActive({ session: res.createdSessionId, }); cy.log(`Finished Signing in.`); }); });
cypress.env.json{ "test_email": "example@clerk.dev", "test_password": "clerkpassword1234" }
Simple tests
Now that some simple commands have been created, let's see how to write some basic tests.
import { SignedIn, SignedOut } from "@clerk/nextjs"; export default function Dashboard() { return ( <> <SignedIn> <h1>Signed in</h1> </SignedIn> <SignedOut> <h1>Signed out</h1> </SignedOut> </> ); }
Test signed out
app.cy.jsdescribe("Signed out", () => { it("should navigate to the dashboard in a signed out state", () => { // open dashboard page cy.visit("http://localhost:3000/dashboard"); // check h1 says signed out cy.get("h1").contains("Signed out"); }); });
Test signed in
Note the use of before
and beforeEach
. You want to be careful about signing in too many times quickly as you might get rate limited. So, you should only sign in once for all your tests inside of before
. Then, since Cypress will clear cookies by default after tests, you need to use beforeEach
to maintain these cookies and keep the browser in the signedIn
state.
app.cy.jsdescribe("Signed in", () => { beforeEach(() => { cy.session('signed-in', () => { cy.signIn(); }); }); it("navigate to the dashboard", () => { // open dashboard page cy.visit("http://localhost:3000/dashboard"); // check h1 says signed in cy.get("h1").contains("Signed in"); }); });
SSR tests
You need to make some slight modifications to test in an SSR context. Because Clerk uses short-lived JWTs, it uses middleware that will re-load the JWT from a different page if necessary. This page might return a 401, so Cypress needs to be told to ignore the 401 and continue. Passing failOnStatusCode: false
into cy.visit
accomplishes just that.
Here's a simple SSR page, where the auth check is done on the server side, and returned to the frontend.
import { withServerSideAuth } from "@clerk/nextjs/ssr"; export const getServerSideProps = withServerSideAuth(async ({ req }) => { const { sessionId, getToken } = req.auth; const sessionToken = await getToken(); return { props: { signedIn: sessionId != null, sessionToken: sessionToken, sessionId: sessionId, }, }; }); export default function Page({ signedIn, sessionToken, sessionId }) { return ( <> <h1>{signedIn ? "Signed in" : "Signed out"}</h1> <div>sessionId: {sessionId}</div> <div>sessionToken: {sessionToken}</div> </> ); }
Test signed out
app.cy.jsdescribe("Signed out", () => { it("should navigate to the ssr page in a signed out state", () => { // open dashboard page cy.visit("http://localhost:3000/dashboard-ssr", { failOnStatusCode: false, }); // check h1 says signed in cy.get("h1").contains("Signed out"); }); })
Test signed in
Note the use of before
and beforeEach
. You want to be careful about signing in too many times quickly as you might get rate limited. So, you should only sign in once for all your tests inside of before
. Then, since Cypress will clear cookies by default after tests, you need to use beforeEach
to maintain these cookies and keep the browser in the signedIn
state.
app.cy.jsdescribe("Signed in", () => { beforeEach(() => { cy.session('signed-in', () => { cy.signIn(); }); }); it("SSR: navigate to the ssr dashboard", () => { // open dashboard page cy.visit("http://localhost:3000/dashboard-ssr", { failOnStatusCode: false, }); // check h1 says signed in cy.get("h1").contains("Signed in"); }); });