Testing a Next.js app that uses Clerk for authentication? Getting mysterious 302 redirects instead of your expected API responses? Here's a clean, minimal pattern that works.
The Problem
Clerk intercepts unauthenticated requests and redirects them to /sign-in. This is great for production, but it means Playwright tests that don't set up auth properly will silently "pass" by landing on a 200 sign-in page instead of actually testing your API.
There are two things you need to solve:
- Global Clerk setup — initialise Clerk's testing infrastructure once before all tests
- Per-test token injection — attach a Clerk testing token to each page so middleware recognises the session
Step 1: Global Setup with clerkSetup
Create e2e/global-setup.ts:
import { clerkSetup } from "@clerk/testing/playwright";
async function globalSetup() {
await clerkSetup();
}
export default globalSetup;
Wire it up in playwright.config.ts:
export default defineConfig({
globalSetup: require.resolve("./e2e/global-setup"),
// ... rest of your config
});
clerkSetup() initialises the Clerk test environment and sets the CLERK_SECRET_KEY env var for token generation. Without this, setupClerkTestingToken will throw.
Step 2: Custom Auth Fixture
Instead of calling setupClerkTestingToken in every test, wrap it in a custom fixture:
// e2e/fixtures/auth.ts
import { test as base } from "@playwright/test";
import { setupClerkTestingToken } from "@clerk/testing/playwright";
export const test = base.extend({
page: async ({ page }, use) => {
await setupClerkTestingToken({ page });
await use(page);
},
});
export { expect } from "@playwright/test";
Now import test from this fixture in your spec files instead of from @playwright/test:
// e2e/dashboard.spec.ts
import { test, expect } from "./fixtures/auth";
test("dashboard loads for authenticated user", async ({ page }) => {
await page.goto("/dashboard");
// Clerk middleware will recognise the token — no redirect
await expect(page.getByText("Your projects")).toBeVisible();
});
Every test using this test automatically gets a Clerk testing token attached to its page. Clean.
Step 3: Testing API Auth (Don't Trust 200s)
Here's a subtle gotcha: if you're testing that unauthenticated API requests are properly rejected, Playwright's default behaviour follows redirects. So a 302 → /sign-in → 200 HTML page will look like a 200 success.
Fix it by disabling redirect following:
const NO_REDIRECT = { maxRedirects: 0, failOnStatusCode: false };
test("unauthenticated GET /api/projects is rejected", async ({ request }) => {
const res = await request.get("/api/projects", NO_REDIRECT);
// Clerk returns 302 to /sign-in — never a 200 API response
expect(res.status()).not.toBe(200);
});
maxRedirects: 0 stops Playwright from following the redirect. failOnStatusCode: false prevents the test from throwing on non-2xx before your assertion runs. Now you're testing actual auth behaviour, not the sign-in page HTML.
When Tests Should Skip vs Fail
If you run E2E tests in CI without a real test user (or without CLERK_SECRET_KEY), tests that require an authenticated session will fail. One pragmatic approach: check for the env var and skip gracefully:
test.beforeEach(async () => {
if (!process.env.CLERK_SECRET_KEY) {
test.skip(true, "No CLERK_SECRET_KEY — skipping auth-dependent tests");
}
});
This way your CI pipeline doesn't break on auth tests that simply can't run without credentials.
Summary
| Concern | Solution |
|---|---|
| Clerk initialisation |
clerkSetup() in globalSetup
|
| Per-test token injection |
setupClerkTestingToken in a custom fixture |
| Avoiding redirect false-positives | maxRedirects: 0, failOnStatusCode: false |
| Skipping without credentials | Check CLERK_SECRET_KEY in beforeEach
|
The fixture pattern is the key insight here — it keeps your tests clean and ensures you never accidentally run an "authenticated" test without the token. One import swap and you're done.
If you found this useful, check out the Clerk Playwright testing docs for deeper configuration options like signIn and signUp helpers.
Top comments (0)