What is Test Isolation?
Test isolation ensures that each test runs independently without affecting or being affected by other tests. This is critical for reliable, maintainable test suites.
Isolated tests can run in any order, in parallel, and produce consistent results.
Why Isolation Matters
Reliability Tests don’t fail due to side effects from other tests
Parallelization Tests can run simultaneously without conflicts
Debugging Failures are easier to debug when tests are independent
Maintainability Tests can be modified without breaking others
Browser Context Isolation
Playwright uses BrowserContext for test isolation:
const browser = await chromium . launch ();
// Test 1 - Isolated context
const context1 = await browser . newContext ();
const page1 = await context1 . newPage ();
await page1 . goto ( 'https://example.com' );
await context1 . close ();
// Test 2 - Fresh isolated context
const context2 = await browser . newContext ();
const page2 = await context2 . newPage ();
await page2 . goto ( 'https://example.com' );
await context2 . close ();
From client/browserContext.ts:58-174, each context provides:
Independent cookies
Separate localStorage/sessionStorage
Isolated cache
Independent network state
Separate permissions
Test Isolation Pattern
Recommended Pattern
import { test } from '@playwright/test' ;
test ( 'user can login' , async ({ page }) => {
// Fresh page for this test
await page . goto ( 'https://example.com/login' );
await page . fill ( '#username' , 'user' );
await page . fill ( '#password' , 'pass' );
await page . click ( '#login' );
await expect ( page ). toHaveURL ( '**/dashboard' );
});
test ( 'user can register' , async ({ page }) => {
// Completely fresh page, no state from previous test
await page . goto ( 'https://example.com/register' );
await page . fill ( '#username' , 'newuser' );
await page . fill ( '#email' , '[email protected] ' );
await page . click ( '#register' );
await expect ( page ). toHaveURL ( '**/welcome' );
});
The { page } fixture automatically creates a fresh browser context and page for each test.
Manual Context Management
const browser = await chromium . launch ();
for ( const testCase of testCases ) {
// Create fresh context for each test
const context = await browser . newContext ();
const page = await context . newPage ();
try {
await runTest ( page , testCase );
} finally {
// Always clean up
await context . close ();
}
}
await browser . close ();
Context Lifecycle
From client/browser.ts:58-90:
async newContext ( options : BrowserContextOptions = {}): Promise < BrowserContext > {
// Prepare options with selectors
const options = this . _browserType . _playwright . selectors . _withSelectorOptions ( userOptions );
// Run before-create hooks
await this._instrumentation.runBeforeCreateBrowserContext(options);
// Create context on server
const contextOptions = await prepareBrowserContextParams ( this . _platform , options );
const response = await this . _channel . newContext ( contextOptions );
// Get context proxy
const context = BrowserContext . from ( response . context );
// Run after-create hooks
await this._instrumentation.runAfterCreateBrowserContext(context);
return context;
}
Each context is:
Created fresh - No shared state
Configured independently - Own options
Tracked separately - Independent lifecycle
Closed cleanly - Resources released
State Management
Cookies
Cookies are isolated per context:
// Test 1
const context1 = await browser . newContext ();
const page1 = await context1 . newPage ();
await page1 . goto ( 'https://example.com' );
await context1 . addCookies ([{ name: 'session' , value: 'abc' , domain: 'example.com' , path: '/' }]);
// Test 2 - No cookies from Test 1
const context2 = await browser . newContext ();
const page2 = await context2 . newPage ();
await page2 . goto ( 'https://example.com' );
const cookies = await context2 . cookies ();
console . log ( cookies . length ); // 0
Storage
localStorage and sessionStorage are isolated:
// Test 1
const context1 = await browser . newContext ();
const page1 = await context1 . newPage ();
await page1 . goto ( 'https://example.com' );
await page1 . evaluate (() => {
localStorage . setItem ( 'key' , 'value1' );
});
// Test 2 - Independent storage
const context2 = await browser . newContext ();
const page2 = await context2 . newPage ();
await page2 . goto ( 'https://example.com' );
const value = await page2 . evaluate (() => {
return localStorage . getItem ( 'key' );
});
console . log ( value ); // null
Network State
Each context has independent network state:
// Test 1 - Offline
const context1 = await browser . newContext ();
await context1 . setOffline ( true );
// Test 2 - Online
const context2 = await browser . newContext ();
// Online by default
Sharing State (When Needed)
Storage State
Sometimes you want to share authentication state:
// Setup: Login once
const setupContext = await browser . newContext ();
const page = await setupContext . newPage ();
await page . goto ( 'https://example.com/login' );
await page . fill ( '#username' , 'user' );
await page . fill ( '#password' , 'pass' );
await page . click ( '#login' );
// Save state
const storageState = await setupContext . storageState ();
await setupContext . close ();
// Test 1: Reuse auth state
const context1 = await browser . newContext ({ storageState });
const page1 = await context1 . newPage ();
await page1 . goto ( 'https://example.com/dashboard' );
// Already logged in!
// Test 2: Also reuse auth state
const context2 = await browser . newContext ({ storageState });
const page2 = await context2 . newPage ();
await page2 . goto ( 'https://example.com/profile' );
// Also logged in!
Even when sharing auth state, contexts remain isolated for other state (new cookies, localStorage changes, etc.).
Persistent Context
For development/debugging, use persistent context:
const context = await chromium . launchPersistentContext ( './user-data' , {
headless: false
});
// State persists across runs
const page = await context . newPage ();
Don’t use persistent context for tests. It breaks isolation.
Resource Cleanup
Automatic Cleanup
Playwright Test automatically cleans up:
import { test } from '@playwright/test' ;
test ( 'my test' , async ({ page , context }) => {
// Use page and context
await page . goto ( 'https://example.com' );
// Automatically closed after test
});
Manual Cleanup
const browser = await chromium . launch ();
const context = await browser . newContext ();
const page = await context . newPage ();
try {
await page . goto ( 'https://example.com' );
// ... test code ...
} finally {
// Always clean up
await page . close ();
await context . close ();
await browser . close ();
}
Async Disposal
const browser = await chromium . launch ();
try {
const context = await browser . newContext ();
try {
const page = await context . newPage ();
await page . goto ( 'https://example.com' );
} finally {
await context . close ();
}
} finally {
await browser . close ();
}
// Or with Symbol.asyncDispose (modern)
await using browser = await chromium . launch ();
await using context = await browser . newContext ();
await using page = await context . newPage ();
// Automatically closed
From client/browser.ts:172-174:
async [ Symbol . asyncDispose ]() {
await this . close ();
}
Parallel Testing
Isolation enables parallel test execution:
import { test } from '@playwright/test' ;
// These run in parallel, completely isolated
test ( 'test 1' , async ({ page }) => {
await page . goto ( 'https://example.com' );
});
test ( 'test 2' , async ({ page }) => {
await page . goto ( 'https://example.com' );
});
test ( 'test 3' , async ({ page }) => {
await page . goto ( 'https://example.com' );
});
With configuration:
// playwright.config.js
export default {
workers: 4 , // Run 4 tests in parallel
fullyParallel: true
} ;
Common Pitfalls
Sharing Browser Context
// BAD: Sharing context between tests
const context = await browser . newContext ();
test ( 'test 1' , async () => {
const page = await context . newPage ();
await page . goto ( 'https://example.com' );
// Sets cookies that affect test 2
});
test ( 'test 2' , async () => {
const page = await context . newPage ();
await page . goto ( 'https://example.com' );
// Has cookies from test 1!
});
// GOOD: Fresh context per test
test ( 'test 1' , async ({ context }) => {
const page = await context . newPage ();
await page . goto ( 'https://example.com' );
});
test ( 'test 2' , async ({ context }) => {
const page = await context . newPage ();
await page . goto ( 'https://example.com' );
});
Global State
// BAD: Global state
let userId ;
test ( 'create user' , async ({ page }) => {
userId = await createUser ( page );
});
test ( 'delete user' , async ({ page }) => {
await deleteUser ( page , userId ); // Depends on previous test!
});
// GOOD: Independent tests
test ( 'create and delete user' , async ({ page }) => {
const userId = await createUser ( page );
await deleteUser ( page , userId );
});
test ( 'user operations' , async ({ page }) => {
const userId = await createUser ( page );
// Use userId in this test
await deleteUser ( page , userId );
});
Test Order Dependencies
// BAD: Order-dependent tests
test ( 'login' , async ({ page }) => {
// Must run first
});
test ( 'use feature' , async ({ page }) => {
// Assumes user is logged in from previous test
});
// GOOD: Self-contained tests
test ( 'login' , async ({ page }) => {
await login ( page );
await expect ( page ). toHaveURL ( '**/dashboard' );
});
test ( 'use feature' , async ({ page }) => {
await login ( page ); // Login in this test too
await useFeature ( page );
});
Best Practices
Always create a fresh context for each test. test ( 'my test' , async ({ page }) => {
// Fresh context and page
});
Always close contexts and browsers. const context = await browser . newContext ();
try {
// test code
} finally {
await context . close ();
}
Don’t share state between tests. // Bad
let shared = {};
// Good - local state
test ( 'my test' , async ({ page }) => {
const local = {};
});
Make Tests Self-Contained
Each test should set up its own prerequisites. test ( 'feature test' , async ({ page }) => {
await login ( page ); // Setup
await useFeature ( page ); // Test
});
Testing with Playwright Test
Playwright Test provides automatic isolation:
import { test , expect } from '@playwright/test' ;
test . describe ( 'User features' , () => {
test ( 'can login' , async ({ page }) => {
// Fresh page for this test
await page . goto ( 'https://example.com' );
});
test ( 'can register' , async ({ page }) => {
// Fresh page for this test
await page . goto ( 'https://example.com' );
});
});
Each test gets:
Fresh browser (or shared based on config)
Fresh context
Fresh page
Automatic cleanup
Isolation Verification
Verify your tests are isolated:
// Run in random order
// playwright.config.js
export default {
testDir: './tests' ,
fullyParallel: true ,
workers: 4
} ;
// Run multiple times
// npm test -- --repeat-each=10
// If tests fail randomly, they lack isolation
Next Steps
Best Practices Testing best practices
Parallelization Running tests in parallel
Test Runner Playwright Test API