← Back to blog
Test automationApril 24, 20265 min

Testing authentication in Playwright (without pain and flaky tests)

Authentication in Playwright is often a major source of flaky tests. How to design login, storageState and multi-user setup so the suite stays stable in CI.

Authentication is one of the most underestimated parts of end-to-end testing. At first it looks simple: log in and continue with the rest of the test. Once the suite grows, though, login quickly becomes one of the main sources of instability.

That is why it makes sense to treat authentication not as just one extra technical step, but as part of the whole testing architecture. If it is designed badly, tests slow down, fail in CI and influence each other. If it is designed well, it significantly improves the stability of the entire suite.

Why authentication becomes a problem

The symptoms are usually similar across projects. Tests work locally but fail in CI. Sessions expire in the middle of a run. Login repeats unnecessarily in every scenario and the whole suite becomes slow. Or tests overwrite each other’s state and failures appear only sometimes.

That is usually not an isolated bug. It is a signal that authentication was added only as a necessary step, not designed as a separate layer.

The slowest anti-pattern is logging in inside every test

The most common starting point looks like this:

test('user can open dashboard', async ({page}) => {
	await page.goto('/login')
	await page.fill('#email', 'test@test.com')
	await page.fill('#password', 'password')
	await page.click('button[type=submit]')
})

This works for the first few tests. For a larger suite, however, it scales very poorly. Login is slow, repeats over and over, and every small delay or instability in the auth flow gets multiplied across the whole regression layer.

A practical foundation: storageState

Playwright allows you to save the authenticated state and reuse it:

await page.context().storageState({path: 'auth.json'})

Then you can attach that state to tests:

test.use({storageState: 'auth.json'})

For most projects, this is the first sensible step. Tests run faster, login no longer repeats all the time and a significant amount of auth-related flakiness disappears.

When storageState alone stops being enough

A shared authenticated state is not a universal solution. Its limits appear as soon as tests modify data, run in parallel or work with multiple user roles. At that point, a shared account stops being safe and tests begin to interfere with each other.

A common example is a suite where you need to separate admin, regular user and guest scenarios. In that case, it makes sense to have separate storageState files per role, or to create users dynamically when needed.

OAuth, MFA and real-world systems

In many applications, authentication is not just email and password. Real systems often include Google login, magic links, MFA or other external identity flows. That is where the difference between theoretical and practical auth testing becomes obvious.

Such scenarios are often much harder to keep stable. Not because of Playwright itself, but because tests now depend on external systems, timing and environments that are not fully controlled by the test suite.

Two paths: mocked login or real login

In practice, teams usually work with two strategies.

Mocking

Mocked login skips the real auth flow and simulates a token or session. It is fast and more stable, but it does not validate the real scenario the user experiences.

Real login

Real login validates the actual flow and gives more confidence. At the same time, it means more setup complexity, more sensitivity to environment issues and more demanding test account management.

The goal is not to choose one universal option. The practical decision is to define which scenarios truly need real authentication and where a faster, more stable mocked layer is sufficient.

Why auth tests fail most often

Most problems repeat. Expired cookies, wrong domain configuration, differences between CI and local environments, or a session state that changes unexpectedly between tests.

That is why it makes sense to log the authentication layer more carefully than regular scenarios. Tracking cookies, the current URL, runtime and similar metadata often makes it much easier to understand whether the issue comes from the application, the test or the environment itself.

What to take into practice

Authentication is not a detail. It is a basic building block of testing architecture. If designed well, it stabilises the whole suite. If designed badly, it becomes one of the biggest sources of flaky tests and wasted time.

In practice, a few basic rules help:

  • do not log in again inside every test
  • use storageState where it makes sense
  • separate users by role and purpose
  • log auth problems systematically
  • expect CI and local environments to behave differently

Summary

At the beginning, login looks like just one technical step. In the long run, it can become either a source of flaky tests and a bottleneck, or a stable foundation for the whole testing strategy.

The difference is not whether you solve authentication in Playwright. The difference is how you design it.

Is your site ready for AI?

Check whether AI tools read your content correctly — or fill in the gaps themselves. A quick test that shows what AI sees and what it ignores.

Test my site

More articles