Modern QA2026pytest + requests: Full Control — tiles
Log inJoin
12 / 65 · 14 API Testing Fundamentals · Newman and pytest for API Automation← prev⊞ allnext →☰ Read as one page

2.3pytest + requests: Full Control

For production API test suites, pytest with the requests library provides the flexibility, maintainability, and power that Newman lacks.

Project Structure

tests/
  api/
    conftest.py           # Shared fixtures
    test_auth.py          # Authentication tests
    test_users.py         # User CRUD tests
    test_orders.py        # Order tests
    test_error_handling.py
  data/
    test_users.csv        # Test data files
  pytest.ini              # Configuration

Shared Fixtures (conftest.py)

import os
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_headers(base_url):
    """Authenticate once per test session."""
    r = requests.post(f"{base_url}/auth/login", json={
        "email": "test@example.com",
        "password": "testpass123"
    })
    assert r.status_code == 200, f"Auth failed: {r.text}"
    token = r.json()["access_token"]
    return {"Authorization": f"Bearer {token}"}

@pytest.fixture
def api(base_url, auth_headers):
    """Pre-configured API session."""
    session = requests.Session()
    session.headers.update(auth_headers)
    session.headers.update({"Content-Type": "application/json"})

    # Store base_url on session for convenience
    session._base_url = base_url
    original_request = session.request

    def patched_request(method, url, **kwargs):
        if url.startswith("/"):
            url = f"{base_url}{url}"
        return original_request(method, url, **kwargs)

    session.request = patched_request
    return session

@pytest.fixture
def create_user(api):
    """Factory fixture for creating test users with cleanup."""
    created_ids = []

    def _create(name="Test User", email=None, role="viewer"):
        import uuid
        email = email or f"test-{uuid.uuid4().hex[:8]}@test.com"
        r = api.post("/users", json={"name": name, "email": email, "role": role})
        assert r.status_code == 201
        user = r.json()
        created_ids.append(user["id"])
        return user

    yield _create

    # Cleanup: delete all created users
    for uid in created_ids:
        api.delete(f"/users/{uid}")

Test Examples

# test_users.py

def test_create_user(api, create_user):
    user = create_user(name="Alice", role="admin")
    assert user["name"] == "Alice"
    assert user["role"] == "admin"
    assert "id" in user

def test_get_user(api, create_user):
    user = create_user(name="Bob")
    r = api.get(f"/users/{user['id']}")
    assert r.status_code == 200
    assert r.json()["name"] == "Bob"

def test_update_user(api, create_user):
    user = create_user(name="Original")
    r = api.put(f"/users/{user['id']}", json={
        "name": "Updated",
        "email": user["email"],
        "role": user["role"]
    })
    assert r.status_code == 200
    assert r.json()["name"] == "Updated"

def test_delete_user(api, create_user):
    user = create_user()
    r = api.delete(f"/users/{user['id']}")
    assert r.status_code == 204
    # Verify deletion
    r = api.get(f"/users/{user['id']}")
    assert r.status_code == 404

@pytest.mark.parametrize("email,expected", [
    ("valid@test.com", 201),
    ("", 400),
    ("not-an-email", 422),
    ("a" * 300 + "@test.com", 422),
])
def test_create_user_email_validation(api, email, expected):
    r = api.post("/users", json={"name": "Test", "email": email, "role": "viewer"})
    assert r.status_code == expected