Learn practical ways to create, manage, reuse, isolate, and clean test data in Playwright Python automation. These examples show real-world patterns for UI, API, database, authentication, parameterized, and end-to-end testing.
Hard-coded data is the simplest approach and can be useful for small, stable scenarios. Avoid relying on it for large or environment-dependent test suites.
def test_customer_login(page):
page.goto("https://shop.example.com/login")
page.get_by_label("Email").fill("qa@example.com")
page.get_by_label("Password").fill("Password123")
page.get_by_role("button", name="Login").click()
Store related test values in dictionaries when a test needs structured customer, product, or order data.
customer = {
"first_name": "John",
"last_name": "Smith",
"email": "john.smith@example.com",
"phone": "9876543210"
}
def test_customer_registration(page):
page.goto("https://shop.example.com/register")
page.get_by_label("First Name").fill(customer["first_name"])
page.get_by_label("Last Name").fill(customer["last_name"])
page.get_by_label("Email").fill(customer["email"])
page.get_by_label("Phone").fill(customer["phone"])
Use pytest fixtures when multiple tests need the same type of test data.
import pytest
@pytest.fixture
def customer_data():
return {
"name": "John Smith",
"email": "john@example.com",
"password": "Password123"
}
def test_customer_profile(page, customer_data):
page.goto("https://shop.example.com/register")
page.get_by_label("Name").fill(customer_data["name"])
page.get_by_label("Email").fill(customer_data["email"])
Generate unique values when tests create records that must not conflict with data created by other tests.
from uuid import uuid4
def create_customer_data():
unique_id = uuid4().hex[:8]
return {
"name": f"Test User {unique_id}",
"email": f"test_{unique_id}@example.com"
}
def test_unique_customer(page):
customer = create_customer_data()
page.goto("https://shop.example.com/register")
page.get_by_label("Name").fill(customer["name"])
page.get_by_label("Email").fill(customer["email"])
The Faker library can generate realistic names, emails, addresses, phone numbers, and other values.
from faker import Faker
fake = Faker()
def test_customer_registration(page):
customer = {
"name": fake.name(),
"email": fake.unique.email(),
"phone": fake.numerify("##########")
}
page.goto("https://shop.example.com/register")
page.get_by_label("Name").fill(customer["name"])
page.get_by_label("Email").fill(customer["email"])
page.get_by_label("Phone").fill(customer["phone"])
Use pytest parameterization when the same test must run against several data combinations.
import pytest
@pytest.mark.parametrize(
"email,password",
[
("valid@example.com", "Password123"),
("customer@example.com", "Password123"),
("admin@example.com", "Admin123")
]
)
def test_login(page, email, password):
page.goto("https://shop.example.com/login")
page.get_by_label("Email").fill(email)
page.get_by_label("Password").fill(password)
page.get_by_role("button", name="Login").click()
JSON files are useful when test data is large, structured, or maintained separately from the test implementation.
[
{
"name": "John Smith",
"email": "john@example.com"
},
{
"name": "Mary Jones",
"email": "mary@example.com"
}
]
import json
def load_customers():
with open("testdata/customers.json", encoding="utf-8") as file:
return json.load(file)
def test_customer_data(page):
customers = load_customers()
for customer in customers:
page.goto("https://shop.example.com/register")
page.get_by_label("Name").fill(customer["name"])
page.get_by_label("Email").fill(customer["email"])
CSV files work well for tabular test data such as usernames, products, prices, or search terms.
name,email,role
John Smith,john@example.com,customer
Mary Jones,mary@example.com,manager
Admin User,admin@example.com,admin
import csv
def load_customers():
with open("testdata/customers.csv", newline="") as file:
return list(csv.DictReader(file))
def test_customer_roles(page):
customers = load_customers()
for customer in customers:
print(customer["name"], customer["role"])
Environment variables are appropriate for environment-specific values such as test accounts, URLs, and configuration.
import os
BASE_URL = os.getenv(
"BASE_URL",
"https://qa.example.com"
)
TEST_EMAIL = os.getenv(
"TEST_EMAIL",
"qa@example.com"
)
TEST_PASSWORD = os.getenv(
"TEST_PASSWORD",
"Password123"
)
def test_login(page):
page.goto(f"{BASE_URL}/login")
page.get_by_label("Email").fill(TEST_EMAIL)
page.get_by_label("Password").fill(TEST_PASSWORD)
Create records through an API when UI-based setup is slow or requires many unnecessary steps.
import pytest
from playwright.sync_api import expect
@pytest.fixture
def customer(api_request_context):
response = api_request_context.post(
"/api/customers",
data={
"name": "API Customer",
"email": "api_customer@example.com"
}
)
response.raise_for_status()
return response.json()
def test_customer_visible_in_ui(page, customer):
page.goto("https://shop.example.com/customers")
expect(
page.get_by_text(customer["name"])
).to_be_visible()
Database setup can be useful for controlled integration tests, especially when creating complex records through the UI would be expensive.
import pytest
@pytest.fixture
def customer(db_connection):
customer_id = create_customer_in_database(
db_connection,
name="Database Customer",
email="db_customer@example.com"
)
yield customer_id
delete_customer_from_database(
db_connection,
customer_id
)
def test_database_customer_appears(page, customer):
page.goto("https://shop.example.com/customers")
page.get_by_text("Database Customer").wait_for()
Factory functions provide a clean way to create test objects with sensible defaults while allowing individual tests to override specific values.
from uuid import uuid4
def customer_factory(**overrides):
unique_id = uuid4().hex[:8]
customer = {
"name": f"Test Customer {unique_id}",
"email": f"customer_{unique_id}@example.com",
"role": "customer"
}
customer.update(overrides)
return customer
def test_manager_profile(page):
customer = customer_factory(
role="manager"
)
print(customer)
Combine factories with pytest fixtures when test data needs consistent creation and automatic cleanup.
import pytest
@pytest.fixture
def customer(api_request_context):
data = customer_factory(
role="customer"
)
response = api_request_context.post(
"/api/customers",
data=data
)
response.raise_for_status()
customer = response.json()
yield customer
api_request_context.delete(
f"/api/customers/{customer['id']}"
)
def test_customer_profile(page, customer):
page.goto(
f"https://shop.example.com/customers/{customer['id']}"
)
Tests should avoid depending on records created by other tests. Generate or provision isolated data whenever possible.
def test_update_customer(page):
customer = customer_factory()
create_customer(customer)
page.goto(
f"https://shop.example.com/customers/{customer['id']}"
)
page.get_by_label("Name").fill("Updated Customer")
page.get_by_role("button", name="Save").click()
Use fixture teardown to remove records created specifically for a test.
import pytest
@pytest.fixture
def test_product():
product = create_product(
name="Temporary Product"
)
yield product
delete_product(product["id"])
def test_product_can_be_archived(test_product):
archive_product(test_product["id"])
assert product_is_archived(
test_product["id"]
)
Maintain separate test data for customer, manager, and admin scenarios when authorization affects application behavior.
import pytest
@pytest.fixture
def admin_data():
return {
"email": "admin@example.com",
"password": "Admin123"
}
@pytest.fixture
def customer_data():
return {
"email": "customer@example.com",
"password": "Customer123"
}
def test_admin_dashboard(page, admin_data):
login(
page,
admin_data["email"],
admin_data["password"]
)
page.goto("https://shop.example.com/admin")
Separate authentication data from ordinary customer data. Tests that require login should receive credentials through fixtures or environment configuration.
import os
import pytest
@pytest.fixture
def test_user():
return {
"email": os.getenv(
"TEST_USER_EMAIL",
"qa@example.com"
),
"password": os.getenv(
"TEST_USER_PASSWORD",
"Password123"
)
}
def test_account_page(page, test_user):
login(
page,
test_user["email"],
test_user["password"]
)
page.goto("https://shop.example.com/account")
Good test-data management also includes invalid, empty, maximum, minimum, and boundary values.
import pytest
@pytest.mark.parametrize(
"email",
[
"",
"invalid-email",
"user@",
"@example.com",
"a" * 300 + "@example.com"
]
)
def test_invalid_email_validation(page, email):
page.goto("https://shop.example.com/register")
page.get_by_label("Email").fill(email)
page.get_by_role("button", name="Register").click()
assert page.get_by_text(
"Enter a valid email address"
).is_visible()
Complex workflows often require multiple related records such as a customer, product, address, cart, and order.
import pytest
@pytest.fixture
def checkout_data(api_request_context):
customer = create_customer(api_request_context)
product = create_product(api_request_context)
address = create_address(api_request_context)
return {
"customer": customer,
"product": product,
"address": address
}
def test_checkout(
authenticated_page,
checkout_data
):
product = checkout_data["product"]
authenticated_page.goto(
f"/products/{product['id']}"
)
authenticated_page.get_by_role(
"button",
name="Add to Cart"
).click()
authenticated_page.goto("/checkout")
A maintainable automation framework normally combines several test-data techniques rather than relying on one approach.
# Stable configuration
ENVIRONMENT = "qa"
# Reusable data factory
customer = customer_factory()
# API/database setup
create_customer(customer)
# UI verification
page.goto("/customers")
# Boundary data
invalid_email = "invalid-email"
# Cleanup
delete_customer(customer["id"])
| Situation | Recommended Approach | Example |
|---|---|---|
| Small stable data | Hard-coded values | Known QA account |
| Reusable data | pytest fixture | Customer fixture |
| Unique records | Dynamic data / Faker | Unique email |
| Many combinations | pytest parameterization | Login scenarios |
| Large structured data | JSON / CSV | Customer datasets |
| Fast backend setup | API fixtures | Create order through API |
| Complex records | Database setup | Order + payment + customer |
| Flexible reusable objects | Factory functions | customer_factory() |
| Temporary records | Fixture + yield cleanup | Temporary order |
| End-to-end workflow | Composite fixtures | Customer + product + order |