Playwright API Automation
When to Use
- Testing REST APIs using the same Playwright setup as UI tests
- Sharing authenticated sessions between API and UI tests
- Seeding test data via API before UI tests
- Validating API responses alongside browser interactions
- Mocking API responses in UI tests
Why Playwright for API Testing
- No extra library — built into Playwright (
APIRequestContext) - Shared auth state — same cookies/tokens used by browser and API calls
- Integrated — seed data via API, verify via UI in the same test
- Fast — API calls without browser overhead
Procedure
1. Create API Client
services/playwright_api_client.py
from playwright.sync_api import APIRequestContext, Playwright
from utils.logger import get_logger
from config.settings import config
import json
logger = get_logger(__name__)
class PlaywrightAPIClient:
"""API client using Playwright's APIRequestContext."""
def __init__(self, request: APIRequestContext):
self.request = request
self.base_url = config.api_base_url
def get(self, endpoint: str, params: dict = None, headers: dict = None):
url = f"{self.base_url}{endpoint}"
response = self.request.get(url, params=params, headers=headers)
logger.info(f"GET {url} → {response.status}")
return response
def post(self, endpoint: str, payload: dict = None, headers: dict = None):
url = f"{self.base_url}{endpoint}"
response = self.request.post(url, data=json.dumps(payload), headers=headers)
logger.info(f"POST {url} → {response.status}")
return response
def put(self, endpoint: str, payload: dict = None, headers: dict = None):
url = f"{self.base_url}{endpoint}"
response = self.request.put(url, data=json.dumps(payload), headers=headers)
logger.info(f"PUT {url} → {response.status}")
return response
def delete(self, endpoint: str, headers: dict = None):
url = f"{self.base_url}{endpoint}"
response = self.request.delete(url, headers=headers)
logger.info(f"DELETE {url} → {response.status}")
return response
def assert_status(self, response, expected: int):
assert response.status == expected, (
f"Expected {expected}, got {response.status}. Body: {response.text()}"
)
def json(self, response):
return response.json()
2. API Fixtures
fixtures/api_fixtures.py
import pytest
from playwright.sync_api import sync_playwright
from services.playwright_api_client import PlaywrightAPIClient
from config.settings import config
@pytest.fixture(scope="session")
def api_request_context():
"""Session-level Playwright API request context."""
with sync_playwright() as pw:
context = pw.request.new_context(
base_url=config.api_base_url,
extra_http_headers={
"Content-Type": "application/json",
"Accept": "application/json",
}
)
yield context
context.dispose()
@pytest.fixture(scope="session")
def api_client(api_request_context):
"""Provide PlaywrightAPIClient for the test session."""
return PlaywrightAPIClient(api_request_context)
@pytest.fixture(scope="session")
def authenticated_api_client():
"""API client with auth token from login endpoint."""
with sync_playwright() as pw:
context = pw.request.new_context(base_url=config.api_base_url)
# Authenticate and capture token
response = context.post(
"/auth/login",
data='{"username": "admin", "password": "pass123"}',
headers={"Content-Type": "application/json"}
)
assert response.status == 200
token = response.json()["access_token"]
context.dispose()
# New context with auth header
auth_context = pw.request.new_context(
base_url=config.api_base_url,
extra_http_headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
)
yield PlaywrightAPIClient(auth_context)
auth_context.dispose()
3. API Test Examples
tests/api/test_users_api.py
import pytest
from playwright.sync_api import expect
@pytest.mark.api
class TestUsersAPI:
"""REST API tests using Playwright APIRequestContext."""
@pytest.mark.smoke
def test_get_all_users(self, api_client):
response = api_client.get("/users")
api_client.assert_status(response, 200)
data = api_client.json(response)
assert isinstance(data, list)
assert len(data) > 0
@pytest.mark.smoke
def test_create_user(self, authenticated_api_client):
payload = {
"name": "Test User",
"email": "testuser@example.com",
"role": "viewer"
}
response = authenticated_api_client.post("/users", payload)
authenticated_api_client.assert_status(response, 201)
data = authenticated_api_client.json(response)
assert data["email"] == payload["email"]
assert "id" in data
@pytest.mark.regression
def test_get_user_by_id(self, api_client):
response = api_client.get("/users/1")
api_client.assert_status(response, 200)
data = api_client.json(response)
assert data["id"] == 1
assert "name" in data
assert "email" in data
@pytest.mark.regression
def test_update_user(self, authenticated_api_client):
update_payload = {"name": "Updated Name"}
response = authenticated_api_client.put("/users/1", update_payload)
authenticated_api_client.assert_status(response, 200)
data = authenticated_api_client.json(response)
assert data["name"] == "Updated Name"
@pytest.mark.regression
def test_delete_user(self, authenticated_api_client):
response = authenticated_api_client.delete("/users/99")
authenticated_api_client.assert_status(response, 204)
def test_get_nonexistent_user(self, api_client):
response = api_client.get("/users/999999")
api_client.assert_status(response, 404)
4. API + UI Combined Test
Seed test data via API, then verify through the browser UI:
tests/test_api_ui_combined.py
import pytest
@pytest.mark.integration
class TestAPIAndUI:
"""Tests that combine API setup with UI verification."""
def test_create_user_via_api_then_verify_in_ui(self, authenticated_api_client, page):
# STEP 1 — Create user via API (fast, reliable)
response = authenticated_api_client.post("/users", {
"name": "E2E Test User",
"email": "e2e_user@example.com"
})
authenticated_api_client.assert_status(response, 201)
user_id = authenticated_api_client.json(response)["id"]
# STEP 2 — Verify user appears in UI
pw_page = page.page
pw_page.goto(f"https://app.example.com/users/{user_id}")
pw_page.wait_for_load_state("networkidle")
from playwright.sync_api import expect
expect(pw_page.get_by_text("E2E Test User")).to_be_visible()
def test_delete_via_api_then_verify_ui_404(self, authenticated_api_client, page):
# STEP 1 — Delete via API
authenticated_api_client.delete("/users/50")
# STEP 2 — Verify 404 page in UI
pw_page = page.page
pw_page.goto("https://app.example.com/users/50")
expect(pw_page.get_by_text("Not Found")).to_be_visible()
5. Mock API in UI Tests
Use Playwright routing to intercept and mock API calls during UI tests:
def test_dashboard_with_mocked_api(page):
pw_page = page.page
# Intercept API and return mock data
pw_page.route("**/api/dashboard/stats", lambda route: route.fulfill(
status=200,
content_type="application/json",
body='{"users": 150, "revenue": 45000, "orders": 320}'
))
pw_page.goto("https://app.example.com/dashboard")
from playwright.sync_api import expect
expect(pw_page.get_by_text("150")).to_be_visible()
expect(pw_page.get_by_text("45000")).to_be_visible()
def test_error_state_with_mocked_api_failure(page):
pw_page = page.page
# Simulate API failure
pw_page.route("**/api/users", lambda route: route.fulfill(
status=500,
body='{"error": "Internal Server Error"}'
))
pw_page.goto("https://app.example.com/users")
from playwright.sync_api import expect
expect(pw_page.get_by_text("Something went wrong")).to_be_visible()
6. Run API Tests
# Run all API tests
pytest tests/api/ -m api
# Run combined API + UI tests
pytest tests/ -m integration
# Run with verbose output
pytest tests/api/ -v --tb=short
# Run with HTML report
pytest tests/api/ --html=reports/api_report.html
Best Practices
- Use
authenticated_api_clientfor protected endpoints - Use API to seed data before UI tests — faster and more reliable
- Use
page.route()to mock APIs when testing UI error states - Always call
context.dispose()in fixture teardown - Share session-level API context to avoid re-auth per test