When running async Playwright tests in Python using async_playwright(), creating a new browser and page for each test makes even basic tests slow (~50s for 3 tests).
Using a single Page object in sync tests is faster, but replicating this in async tests is tricky.
Original async test code:
from playwright.async_api import async_playwright
def test_has_title():
with async_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context()
page = context.new_page()
# Go to the Playwright documentation page
page.goto("https://playwright.dev/")
print(page.title())
Original sync test code (faster):
from playwright.sync_api import Page
def test_has_title(page: Page):
page.goto("https://playwright.dev/")
print(page.title())
Attempted async approach using a shared Page object (didn’t work):
import pytest
from playwright.async_api import Page
@pytest.mark.asyncio
async def test_has_title(page: Page):
await page.goto("https://playwright.dev/")
print(await page.title())
Package versions:
- playwright 1.51.0
- pytest 8.3.5
- pytest-asyncio 0.26.0
- pytest-base-url 2.1.0
- pytest-dotenv 0.5.2
- pytest-playwright 0.7.0
I ran into the same issue when moving from sync to async tests. The key is to leverage the page fixture from pytest-playwright, which handles the browser and context setup for you.
You can use it in async tests like this:
import pytest
@pytest.mark.asyncio
async def test_has_title(page):
await page.goto("https://playwright.dev/")
title = await page.title()
print(title)
pytest-playwright ensures that a single browser context is shared between tests if you configure the scope="session" in your own fixture, so tests run much faster than launching a browser each time.
In my async tests, I created a session-scoped fixture that launches the browser once and returns a Page object to all tests:
import pytest
from playwright.async_api import async_playwright
@pytest.fixture(scope="session")
async def page():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
context = await browser.new_context()
page = await context.new_page()
yield page
await browser.close()
@pytest.mark.asyncio
async def test_title(page):
await page.goto("https://playwright.dev/")
print(await page.title())
@pytest.mark.asyncio
async def test_url(page):
await page.goto("https://playwright.dev/docs/intro")
print(await page.url())
This way, the same browser and page are reused, dramatically speeding up test execution.
I like to share the browser context instead of the exact Page for async tests. That way, each test still gets a clean page but you avoid launching the browser multiple times:
@pytest.fixture(scope="session")
async def context():
async with async_playwright() as p:
browser = await p.chromium.launch()
context = await browser.new_context()
yield context
await browser.close()
@pytest.mark.asyncio
async def test_one(context):
page = await context.new_page()
await page.goto("https://playwright.dev/")
print(await page.title())
await page.close()
@pytest.mark.asyncio
async def test_two(context):
page = await context.new_page()
await page.goto("https://playwright.dev/docs/api/class-playwright")
print(await page.title())
await page.close()
This gives a good balance between speed and test isolation, because each test still gets a fresh page without repeatedly starting the browser.