Skip to content

Chapter 15 · pytest Integration Testing ​

Chapter goals: Master pytest-httpx plugin and pytest-asyncio to write reproducible, parallelizable HTTP client tests.

15.1 Why HTTP Tests Need Dedicated Tools ​

Making real HTTP requests for testing has three problems:

  1. Slow: every test waits for network I/O;
  2. Unstable: depends on external service availability, prone to flakiness;
  3. Side effects: may produce real data (e.g., creating users, charging).

The solution is to intercept and mock the network layer. Both pytest-httpx and httpx's built-in MockTransport can do this.

15.2 pytest-httpx Plugin: Match and Return ​

pytest-httpx is the most popular pytest plugin, providing request interception via fixtures:

bash
pip install pytest-httpx httpx
python
# tests/test_api_client.py
import pytest
import httpx
from myapp.client import GitHubClient


def test_get_user(consumer: httpx.Client) -> None:
    """Intercept requests via fixture, return preset response"""
    # Register expected request and response
    consumer.register_pattern(
        httpx.MatchRequest("GET", "https://api.github.com/users/octocat"),
        httpx.Response(200, json={"login": "octocat", "stars": 1000}),
    )
    
    client = GitHubClient()
    user = client.get_user("octocat")
    
    assert user["login"] == "octocat"
    assert user["stars"] == 1000

Match Granularity Control

MatchRequest supports precise matching of URL, method, headers, and body — ideal for tests that need to distinguish different request scenarios.

15.3 Shorthand Fixtures: client and async_client ​

pytest-httpx provides two convenient fixtures:

python
import pytest
import httpx


def test_simple_get(client: httpx.Client) -> None:
    """client fixture auto-registers patterns"""
    client.register(
        httpx.Response(200, json={"ok": True}),
        match=[httpx.MatchRequest(url="https://httpbin.org/get")]
    )
    resp = client.get("https://httpbin.org/get")
    assert resp.json() == {"ok": True}


@pytest.mark.asyncio
async def test_async_get(async_client: httpx.AsyncClient) -> None:
    """async_client fixture for async tests"""
    async_client.register(
        httpx.Response(200, json={"async": True}),
        match=[httpx.MatchRequest(url="https://httpbin.org/get")]
    )
    resp = await async_client.get("https://httpbin.org/get")
    assert resp.json() == {"async": True}

15.4 Using MockTransport: Pure httpx Approach ​

Without relying on third-party plugins, httpx has built-in MockTransport:

python
import httpx


def mock_handler(request: httpx.Request) -> httpx.Response:
    if "/users/" in str(request.url):
        return httpx.Response(200, json={"id": 1, "name": "Alice"})
    return httpx.Response(404)


def test_with_mock_transport() -> None:
    with httpx.Client(transport=httpx.MockTransport(mock_handler)) as client:
        resp = client.get("https://api.example.com/users/1")
        assert resp.status_code == 200
        assert resp.json()["name"] == "Alice"
        
        resp = client.get("https://api.example.com/missing")
        assert resp.status_code == 404

MockTransport is suitable for simple scenarios that don't need pytest fixtures, or when using non-pytest test frameworks.

15.5 Testing Async Clients ​

Combine with pytest-asyncio to test async code:

bash
pip install pytest-asyncio
python
# Configure asyncio mode in conftest.py
import pytest

@pytest.fixture(scope="session")
def asyncio_mode():
    return "auto"  # auto-detect async def tests
python
import pytest
import httpx
import pytest_asyncio


@pytest.mark.asyncio
async def test_async_flow(async_client: httpx.AsyncClient) -> None:
    async_client.register(
        httpx.Response(200, json={"token": "abc123"}),
        match=[httpx.MatchRequest(url="https://auth.example.com/token")]
    )
    
    async with async_client as client:
        resp = await client.post(
            "https://auth.example.com/token",
            json={"username": "user", "password": "pass"}
        )
        assert resp.json()["token"] == "abc123"

15.6 Complete Test Example: API Client ​

python
# myapp/client.py
import httpx

class APIClient:
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url
        self.api_key = api_key
    
    def get_user(self, user_id: int) -> dict:
        resp = httpx.get(
            f"{self.base_url}/users/{user_id}",
            headers={"Authorization": f"Bearer {self.api_key}"}
        )
        resp.raise_for_status()
        return resp.json()


# tests/test_client.py
import pytest
import httpx


def test_get_user_success(client: httpx.Client) -> None:
    client.register(
        httpx.Response(200, json={"id": 42, "name": "Bob"}),
        match=[
            httpx.MatchRequest(
                url="https://api.example.com/users/42",
                headers={"Authorization": "Bearer test-key"}
            )
        ]
    )
    
    from myapp.client import APIClient
    api = APIClient("https://api.example.com", "test-key")
    user = api.get_user(42)
    
    assert user["name"] == "Bob"
    assert user["id"] == 42


def test_get_user_not_found(client: httpx.Client) -> None:
    client.register(
        httpx.Response(404, json={"error": "not found"}),
        match=[httpx.MatchRequest(url="https://api.example.com/users/999")]
    )
    
    from myapp.client import APIClient
    api = APIClient("https://api.example.com", "test-key")
    
    with pytest.raises(httpx.HTTPStatusError):
        api.get_user(999)

15.7 Chapter Summary ​

  • pytest-httpx plugin provides client/async_client fixtures and register/register_pattern methods;
  • MockTransport is httpx's built-in solution with no extra dependencies;
  • Combine with pytest-asyncio to test async clients;
  • Tests should cover success, client errors (4xx), and server errors (5xx) paths.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. Does the pytest-httpx client fixture automatically send real requests by default?

2. What is the handler function signature for httpx.MockTransport?

3. What is the correct decorator for testing async def functions in pytest-asyncio?

4. What is the main difference between register_pattern and register?

🛠️ Hands-on Practice ​

  1. Use pytest-httpx to test a simple REST client wrapping GET/POST, covering both success and 404 cases.
  2. Use MockTransport to implement an async HTTP test without depending on pytest-httpx.
  3. Write a test that verifies the client raises the correct exception with error message when the service returns 503.

After completing the exercises, move to Next chapter: Performance Optimization and Resource Management.