Skip to content

Testing Guide

This guide covers testing practices and patterns for strapi-kit.

Running Tests

Quick Start

# Run all tests
make test

# Run with coverage
make coverage

# Run specific test file
pytest tests/unit/test_client.py -v

# Run specific test
pytest tests/unit/test_client.py::TestSyncClient::test_get_request_success -v

Test Commands

# Verbose output
pytest -v

# Show local variables on failure
pytest --showlocals

# Stop on first failure
pytest -x

# Run only failed tests from last run
pytest --lf

# Watch mode (requires pytest-watch)
pytest-watch

Test Organization

tests/
├── unit/               # Fast, isolated unit tests
│   ├── test_client.py
│   ├── test_config.py
│   └── test_auth.py
├── integration/        # Tests against real Strapi (future)
│   └── test_e2e.py
└── conftest.py         # Shared fixtures

Test Categories

Unit Tests: - Test single components in isolation - Mock external dependencies - Fast (<1 second each) - No network calls

Integration Tests (Planned): - Test against real Strapi instance - Use Docker for Strapi setup - Slower but verify real behavior

Writing Tests

Test Naming

# Pattern: test_<action>_<expected_result>
def test_get_articles_success():
    """Test successful retrieval of articles."""
    pass

def test_get_articles_not_found():
    """Test handling of 404 when articles don't exist."""
    pass

def test_authentication_with_invalid_token():
    """Test that invalid token raises AuthenticationError."""
    pass

Test Structure (AAA Pattern)

def test_example():
    # Arrange - Set up test data and mocks
    config = StrapiConfig(base_url="http://test", api_token="token")
    expected_data = {"id": 1, "title": "Test"}

    # Act - Execute the code being tested
    with SyncClient(config) as client:
        response = client.get("articles")

    # Assert - Verify the results
    assert response["data"][0]["id"] == expected_data["id"]

HTTP Mocking with respx

Basic Mocking

import pytest
import httpx
import respx

@pytest.mark.respx
def test_get_request(respx_mock):
    """Test basic GET request."""
    # Mock the response
    respx_mock.get("http://localhost:1337/api/articles").mock(
        return_value=httpx.Response(200, json={"data": []})
    )

    # Make request (will use mock)
    config = StrapiConfig(base_url="http://localhost:1337", api_token="test")
    with SyncClient(config) as client:
        response = client.get("articles")

    assert response["data"] == []

Mocking Errors

@pytest.mark.respx
def test_authentication_error(respx_mock):
    """Test handling of authentication errors."""
    # Mock 401 response
    respx_mock.get("http://localhost:1337/api/articles").mock(
        return_value=httpx.Response(401, json={
            "error": {"message": "Invalid token"}
        })
    )

    config = StrapiConfig(base_url="http://localhost:1337", api_token="invalid")
    with SyncClient(config) as client:
        with pytest.raises(AuthenticationError) as exc_info:
            client.get("articles")

    assert "Invalid token" in str(exc_info.value)

Multiple Requests

@pytest.mark.respx
def test_multiple_requests(respx_mock):
    """Test making multiple requests."""
    # Mock multiple endpoints
    respx_mock.get("http://localhost:1337/api/articles").mock(
        return_value=httpx.Response(200, json={"data": [{"id": 1}]})
    )
    respx_mock.get("http://localhost:1337/api/users").mock(
        return_value=httpx.Response(200, json={"data": [{"id": 2}]})
    )

    config = StrapiConfig(base_url="http://localhost:1337", api_token="test")
    with SyncClient(config) as client:
        articles = client.get("articles")
        users = client.get("users")

    assert len(articles["data"]) == 1
    assert len(users["data"]) == 1

Testing Async Code

Async Test Pattern

import pytest

@pytest.mark.asyncio
@pytest.mark.respx
async def test_async_get_request(respx_mock):
    """Test async GET request."""
    respx_mock.get("http://localhost:1337/api/articles").mock(
        return_value=httpx.Response(200, json={"data": []})
    )

    config = StrapiConfig(base_url="http://localhost:1337", api_token="test")
    async with AsyncClient(config) as client:
        response = await client.get("articles")

    assert response["data"] == []

Note: pytest-asyncio is configured in auto mode, so @pytest.mark.asyncio is optional but recommended for clarity.

Fixtures

Using Shared Fixtures

# Defined in conftest.py
def test_with_config(strapi_config):
    """Test using shared config fixture."""
    with SyncClient(strapi_config) as client:
        assert client.config.base_url == "http://localhost:1337"

def test_with_mock_response(mock_v4_response):
    """Test using shared v4 response fixture."""
    assert "attributes" in mock_v4_response["data"]

Creating Custom Fixtures

# In conftest.py or test file
import pytest

@pytest.fixture
def article_data():
    """Sample article data for testing."""
    return {
        "id": 1,
        "attributes": {
            "title": "Test Article",
            "content": "Test content"
        }
    }

# Use in tests
def test_article_parsing(article_data):
    assert article_data["attributes"]["title"] == "Test Article"

Parametrized Tests

Testing Multiple Cases

import pytest

@pytest.mark.parametrize("status_code,exception_type", [
    (401, AuthenticationError),
    (403, AuthorizationError),
    (404, NotFoundError),
    (400, ValidationError),
    (500, ServerError),
])
@pytest.mark.respx
def test_error_handling(status_code, exception_type, respx_mock):
    """Test that different status codes raise correct exceptions."""
    respx_mock.get("http://localhost:1337/api/test").mock(
        return_value=httpx.Response(status_code, json={"error": "Test error"})
    )

    config = StrapiConfig(base_url="http://localhost:1337", api_token="test")
    with SyncClient(config) as client:
        with pytest.raises(exception_type):
            client.get("test")

Coverage Guidelines

Target Coverage

  • Overall: 85%+
  • New features: 100%
  • Critical paths: 100% (auth, error handling)
  • Edge cases: As needed

Running Coverage

# Generate HTML report
make coverage

# View report
open htmlcov/index.html

# Generate XML for CI
pytest --cov=strapi_kit --cov-report=xml

Coverage Configuration

Exclude lines from coverage:

def example():
    if TYPE_CHECKING:  # pragma: no cover
        from typing import Protocol

    if __name__ == "__main__":  # pragma: no cover
        main()

    raise NotImplementedError  # pragma: no cover

Testing Best Practices

Do's ✅

  • Test both success and error paths
  • Test both sync and async variants
  • Use descriptive test names
  • Keep tests fast (<1 second each)
  • Mock external dependencies
  • Test edge cases
  • Use fixtures for common setup

Don'ts ❌

  • Don't test implementation details
  • Don't make tests dependent on each other
  • Don't use real network calls in unit tests
  • Don't test framework code (httpx, pydantic)
  • Don't skip tests without good reason
  • Don't use sleep() in tests

Example: Good Test

@pytest.mark.respx
def test_get_articles_with_filters(respx_mock, strapi_config):
    """Test GET request with query parameters."""
    # Arrange
    respx_mock.get(
        "http://localhost:1337/api/articles",
        params={"filters[title][$eq]": "Test"}
    ).mock(
        return_value=httpx.Response(200, json={
            "data": [{"id": 1, "attributes": {"title": "Test"}}]
        })
    )

    # Act
    with SyncClient(strapi_config) as client:
        response = client.get("articles", params={
            "filters[title][$eq]": "Test"
        })

    # Assert
    assert len(response["data"]) == 1
    assert response["data"][0]["attributes"]["title"] == "Test"

Debugging Tests

Using pdb

def test_something():
    config = StrapiConfig(base_url="http://test", api_token="token")

    # Drop into debugger
    import pdb; pdb.set_trace()

    with SyncClient(config) as client:
        response = client.get("articles")
def test_with_output(capsys):
    """Test that captures print output."""
    print("Debug info")
    result = some_function()

    # Capture and check output
    captured = capsys.readouterr()
    assert "Debug info" in captured.out

Verbose Errors

# Show full diff for assertion errors
pytest --tb=long

# Show local variables on failure
pytest --showlocals

# Show captured output
pytest -s

Continuous Integration

Tests run automatically on: - Every pull request - Every push to main/dev - Scheduled nightly builds

Required Checks: - All tests pass - Coverage > 85% - No type errors (mypy) - No linting errors (ruff)

Further Reading