Skip to content

Pytest for API Testing Deep Dive — Testing Python APIs with httpx and pytest

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Pytest for API Testing Deep Dive. We cover key concepts, practical examples, and best practices to help you master this topic.

Pytest with httpx provides a modern, async-native approach to API testing in Python, combining pytest's powerful fixture system and parametrization with httpx's fast HTTP client.

Code Example: Basic API Tests with httpx

import pytest
import httpx

BASE_URL = "https://api.durga-antivirus.com"

class TestThreatAPI:
    @pytest.fixture
    def client(self):
        return httpx.Client(base_url=BASE_URL, timeout=30.0)

    @pytest.fixture
    def auth_headers(self, client):
        resp = client.post("/api/auth/login", json={
            "username": "test-analyst",
            "password": "test-password"
        })
        assert resp.status_code == 200
        token = resp.json()["access_token"]
        return {"Authorization": f"Bearer {token}"}

    def test_list_threats(self, client, auth_headers):
        response = client.get("/api/v1/threats", headers=auth_headers)
        assert response.status_code == 200
        data = response.json()
        assert "threats" in data
        assert "total" in data

    def test_create_threat(self, client, auth_headers):
        response = client.post(
            "/api/v1/threats",
            json={
                "name": "Pytest Integration Test",
                "severity": "medium",
                "source_ip": "10.0.0.55"
            },
            headers=auth_headers
        )
        assert response.status_code == 201
        data = response.json()
        assert data["name"] == "Pytest Integration Test"
        assert "id" in data

Code Example: Parametrized Test Cases

@pytest.mark.parametrize("name,severity,source_ip,expected_status", [
    ("SQL Injection", "high", "192.168.1.1", 201),
    ("XSS Attack", "medium", "10.0.0.50", 201),
    ("", "low", "10.0.0.1", 422),  # Empty name
    ("Valid Name", "", "10.0.0.1", 422),  # Empty severity
    ("Long Name " + "x" * 200, "low", "10.0.0.1", 422),  # Too long name
    ("Valid", "low", "not-an-ip", 422),  # Invalid IP
])
def test_threat_creation_parameterized(
    client, auth_headers, name, severity, source_ip, expected_status
):
    payload = {"name": name, "severity": severity, "source_ip": source_ip}
    # Remove empty values to test missing field validation
    payload = {k: v for k, v in payload.items() if v}

    response = client.post(
        "/api/v1/threats",
        json=payload,
        headers=auth_headers
    )
    assert response.status_code == expected_status

    if expected_status == 201:
        data = response.json()
        assert data["name"] == name
        assert "id" in data

Code Example: Async httpx Client

import pytest
import httpx
import asyncio

class TestAsyncThreatAPI:
    @pytest.fixture
    async def async_client(self):
        async with httpx.AsyncClient(
            base_url=BASE_URL, timeout=30.0
        ) as client:
            yield client

    @pytest.mark.asyncio
    async def test_async_threat_creation(self, async_client):
        response = await async_client.post(
            "/api/v1/threats",
            json={
                "name": "Async Threat Test",
                "severity": "high",
                "source_ip": "10.0.0.99"
            },
            headers={"Authorization": "Bearer test-token"}
        )
        assert response.status_code == 201
        data = response.json()
        assert data["name"] == "Async Threat Test"

    @pytest.mark.asyncio
    async def test_concurrent_threat_creation(self, async_client):
        threats = [
            {"name": f"Concurrent Threat {i}", "severity": "low",
             "source_ip": f"10.0.0.{i}"}
            for i in range(10)
        ]

        tasks = [
            async_client.post(
                "/api/v1/threats",
                json=t,
                headers={"Authorization": "Bearer test-token"}
            )
            for t in threats
        ]

        responses = await asyncio.gather(*tasks)
        for resp in responses:
            assert resp.status_code == 201

Common Mistakes

1. Using Requests Instead of httpx

requests is synchronous and blocks the event loop. Use httpx for async tests and better performance with connection pooling.

2. Not Using Pytest Fixtures for Auth

Duplicating login logic in every test creates maintenance overhead. Use a fixture that returns auth headers and reuse it.

3. Hardcoding Test Data

Use pytest parametrization for multiple test scenarios. Hardcoded values make it hard to add new test cases.

4. No Response Time Assertions

APIs that return correct data but are too slow degrade user experience. Add response time assertions: assert response.elapsed.total_seconds() < 1.0.

5. Not Testing Error Responses

Test that the API returns proper error structures for 4xx and 5xx responses. Validate error message format and status codes.

Practice Questions

  1. Why is httpx preferred over requests for API testing?
  2. How does pytest parametrization reduce test code duplication?
  3. How do async API tests improve performance?
  4. What is the purpose of the pytest fixture scope?
  5. How do you generate HTML test reports with pytest?

Answers:

  1. httpx supports async/await, connection pooling, HTTP/2, and has a more modern API. It is faster for concurrent requests and integrates better with async applications.
  2. Parametrization runs the same test function with different inputs. A single parametrized test replaces multiple similar test functions.
  3. Async tests can run concurrent requests without blocking. 10 concurrent requests complete in the time of 1 sequential request.
  4. Fixture scope (function, class, module, session) controls how often the fixture is created. Session-scoped fixtures (auth token) improve performance by creating once.
  5. Use pytest --html=report.html. Install pytest-html: pip install pytest-html. HTML reports include test names, status, duration, and error details.

Challenge: Build a comprehensive pytest test suite for a FastAPI threat intelligence API with async httpx client, parametrized test cases, fixture-based authentication, response time assertions, and HTML test reporting.

FAQ

Can pytest test FastAPI applications directly?

Yes. Use FastAPI's TestClient with httpx. TestClient wraps the ASGI application and makes requests without running a server.

How do I mock external APIs in pytest?

Use pytest-httpx or responses library to mock HTTP calls. Replace the real API response with controlled test data.

How do I run tests in parallel with pytest?

Use pytest-xdist: pytest -n auto. Tests in different classes run in parallel. Ensure tests are isolated and do not share state.

What is pytest-cov and how do I use it?

pytest-cov measures code coverage. Run: pytest --cov=src --cov-report=html. Generates an HTML coverage report showing which lines are tested.

Should I use sync or async client for API tests?

Use async for applications built with async frameworks (FastAPI, Starlette). Use sync for Flask, Django, or simple request-response tests.

Mini Project

Build a pytest test suite for a FastAPI threat intelligence API with: async httpx client, fixture-based auth, parametrized test cases (20+ scenarios), concurrent request tests, response time assertions, and HTML coverage reports.

What's Next

Now learn about Pytest Fixtures for API Testing for building reusable test components.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro