A sign-in problem can block every protected journey your suite tries to exercise. Login handling can contribute to flaky suites: they re-authenticate in every test, share one account across parallel workers, or drive a third-party OAuth screen that was never built for automation. Here are the patterns that keep auth tests fast and honest.
Use dedicated test identities, never real ones
Give your suite its own accounts. Not a developer's personal login, not a shared admin, not a customer record you found in staging. A dedicated test identity has three properties that matter:
- It's disposable. You can reset its data, wipe its cart, or let a test corrupt its state without paging anyone.
- It's scoped. Give it exactly the permissions the flow under test needs. A test that exercises the checkout page shouldn't run as a superuser.
- It's isolated per concern. If two suites need different states — one "empty" account, one "account with 200 orders" — make them two identities, not one you keep mutating.
The credentials for those identities are secrets. They belong in your CI secret store or an environment variable, never hardcoded in a spec and never committed. In Playwright, read them at runtime:
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
Session reuse versus fresh login per test
One lever on suite speed is not logging in for every test. Logging in is slow, it's network-bound, and it exercises the same three fields a thousand times to tell you nothing new. Authenticate once, save the resulting session, and reuse it.
Playwright models this cleanly with a setup project that logs in and writes storageState (cookies plus localStorage) to disk. Every other project depends on it and starts already authenticated.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' },
dependencies: ['setup'],
testIgnore: /auth\.setup\.ts/,
},
],
});
// auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import path from 'path';
const authFile = path.join(__dirname, '../.auth/user.json');
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait for something that only exists once you're in.
await expect(page.getByRole('button', { name: 'Account' })).toBeVisible();
await page.context().storageState({ path: authFile });
});
Keep .auth/ out of version control because saved state contains credentials. Tests in this project start with saved state, subject to expiry, account state and backend session policy.
The one place you should still log in for real is the login test itself. Keep a small, explicit suite that fills the form, checks bad-password errors, verifies the redirect, and confirms logout clears the session. Force that suite to start signed-out so it's testing what it claims to:
test.use({ storageState: { cookies: [], origins: [] } });
The rule of thumb: one test proves login works; every other test assumes it does.
Handle expiry and refresh deliberately
Sessions end. Tokens expire, refresh tokens rotate, and "remember me" changes the rules. If you never test the boundary, you'll ship the bug where an expired session dumps the user on a blank page instead of the login screen.
This example checks cookie removal. It does not reproduce expiry, refresh-token rotation or auth kept in localStorage; test those mechanisms separately:
test('missing auth cookies send the user back to login', async ({ page, context }) => {
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
// Simulate the session dying between requests.
await context.clearCookies();
await page.getByRole('link', { name: 'Settings' }).click();
await expect(page).toHaveURL(/\/login/);
await expect(page.getByText('Please sign in to continue')).toBeVisible();
});
If you use short-lived access tokens with silent refresh, add a test that waits past the access-token lifetime and confirms an in-page action still succeeds — that's the difference between a working refresh loop and a silent logout your users will hate. Keep token lifetimes configurable in your test environment so this test takes seconds, not the production 15 minutes.
OAuth in tests: don't drive the provider
The "Continue with Google" and "Continue with GitHub" buttons are a trap for browser automation. The provider's consent screen can present bot checks and other automation limits: it changes markup without notice, throws CAPTCHAs and device checks, and enforces MFA you can't script. A test that clicks through the real Google login is slow, flaky, and one UI redesign away from red.
Two patterns keep you sane:
- Bypass the provider for functional tests. Seed the authenticated session directly — via a test-only login endpoint, an API call that mints a session, or a captured
storageState— so your app opens already signed in. Your tests then exercise your app, which is what you actually own. - Keep one real smoke test, isolated and allowed to be slow. If you must prove the OAuth round-trip works end to end, use a provider that supports test accounts, run it separately from the main suite, and don't let it gate every deploy. Accept that it will occasionally need a human.
The principle: test your authorization logic (what a logged-in user can see and do), not the identity provider's login screen. That screen is their job to test.
Lockout traps that quietly break suites
Auth systems fight back against exactly the traffic a test suite generates, and the failures look like flakiness:
- Rate limits and lockout. Your login test deliberately submits wrong passwords — and trips whatever lockout rule your app enforces, breaking every later test using that identity. Fix: give the negative-path tests their own throwaway identity, separate from the one your happy-path suite reuses.
- Parallel workers on one account. Run four workers that each log in as
test@example.comand the backend may invalidate older sessions when a new one appears, or hit per-account concurrency caps. Either pool one identity per worker, or authenticate once and share the savedstorageState(which sidesteps re-login entirely). - CAPTCHA after N attempts. If a bot check appears mid-suite, don't try to solve it. Whitelist your test environment's IPs or disable the challenge for known test identities in non-production configs.
- Email and SMS verification. Real inboxes and phones don't belong in a fast suite. Use a mailbox API or a test-only bypass for the codes, and keep the true end-to-end verification as a rare, separate check.
The common thread is separation: negative tests, parallel workers, and verification flows each want their own isolated identity so one test's side effect never poisons another's.
Where TestVibe fits
When you describe an auth flow in plain language, TestVibe generates the Playwright test by driving a live browser session against your app, then you run that generated test to see it pass — the behavior is verified by execution, not asserted on faith. Credentials stay out of the test text: store them in your project's Variables & Secrets and reference them from Gherkin with {{secret:TEST_USER_PASSWORD}} tokens resolved in the execution environment instead of literal values in the feature file. Known secrets are redacted from transcripts, but sensitive page content can still appear in browser evidence.
Keep credentials in project-scoped secrets and use their references in the test instructions. Check the target environment before running, and keep sensitive values out of authored code.
Because a signed-in flow is reused constantly, you can model shared setup, but explicitly configure reusable authentication state where supported; dependency completion alone does not imply the same browser session.
Auth testing rewards discipline over cleverness: dedicated identities, log in once, test the expiry boundary on purpose, and keep the third-party provider out of your critical path. Get those four right and you reduce several avoidable auth-related failure modes. Get early access to try it against your own login, or read more in the TestVibe docs.