Modern QA2026pytest: The Test Framework — tiles
Log inJoin
3 / 80 · 12 Programming for QA · Python Essentials for QA← prev⊞ allnext →☰ Read as one page

1.3pytest: The Test Framework

pytest is the standard Python test framework. It is simpler than unittest, more powerful, and used by the majority of Python QA teams.

Basic Test Structure

# test_login.py
def test_successful_login(api_client):
    response = api_client.post("/auth/login", json={
        "email": "test@example.com",
        "password": "ValidPass123!"
    })
    assert response.status_code == 200
    assert "access_token" in response.json()

def test_login_with_invalid_password(api_client):
    response = api_client.post("/auth/login", json={
        "email": "test@example.com",
        "password": "wrong"
    })
    assert response.status_code == 401
    assert "access_token" not in response.json()

Fixtures

Fixtures provide reusable setup and teardown. They replace the setup/teardown methods of unittest with a more flexible, composable pattern.

# conftest.py
import pytest
import requests

@pytest.fixture(scope="session")
def base_url():
    return os.environ.get("API_BASE_URL", "http://localhost:3000/api/v1")

@pytest.fixture(scope="session")
def auth_token(base_url):
    r = requests.post(f"{base_url}/auth/login", json={
        "email": "admin@test.com", "password": "adminpass"
    })
    return r.json()["access_token"]

@pytest.fixture
def auth_headers(auth_token):
    return {"Authorization": f"Bearer {auth_token}"}

@pytest.fixture
def api_client(base_url, auth_headers):
    session = requests.Session()
    session.headers.update(auth_headers)
    session.base_url = base_url
    return session

Fixture scopes control lifecycle:

  • scope="function" (default): runs before/after each test
  • scope="class": once per test class
  • scope="module": once per file
  • scope="session": once per test run

Parametrize

Run the same test with different data:

@pytest.mark.parametrize("email,expected_status", [
    ("valid@test.com", 200),
    ("", 400),
    ("not-an-email", 422),
    ("valid@test.com; DROP TABLE users", 422),
    ("a" * 255 + "@test.com", 422),
])
def test_login_email_validation(api_client, email, expected_status):
    response = api_client.post("/auth/login", json={
        "email": email, "password": "ValidPass123!"
    })
    assert response.status_code == expected_status

Markers

Tag tests for selective execution:

@pytest.mark.smoke
def test_health_check(base_url):
    assert requests.get(f"{base_url}/health").status_code == 200

@pytest.mark.slow
def test_full_checkout_flow(api_client):
    # ... lengthy test
    pass
pytest -m smoke          # Run only smoke tests
pytest -m "not slow"     # Skip slow tests