Skip to content

Testing Patterns Guide

Testing strategies and patterns used across the provide.io ecosystem.

๐Ÿค– AI-Generated Content

This documentation was generated with AI assistance and is still being audited. Some, or potentially a lot, of this information may be inaccurate. Learn more.

Overview

All projects use pytest with async support and follow similar testing patterns. This guide covers common approaches and project-specific considerations.

Standard Testing Setup

Basic Test Structure

import pytest
from pathlib import Path

def test_basic_functionality():
    """Test basic feature behavior."""
    result = my_function("input")
    assert result == "expected"

@pytest.mark.asyncio
async def test_async_functionality():
    """Test async features."""
    result = await my_async_function()
    assert result is not None

Using Fixtures

@pytest.fixture
def sample_data():
    """Provide test data."""
    return {"key": "value"}

@pytest.fixture
def temp_dir(tmp_path):
    """Provide temporary directory."""
    return tmp_path

def test_with_fixtures(sample_data, temp_dir):
    """Test using fixtures."""
    assert sample_data["key"] == "value"
    assert temp_dir.exists()

Foundation Testing Patterns

Foundation Reset (CRITICAL)

Always reset Foundation state in tests:

import pytest
from provide.testkit import reset_foundation_setup_for_testing

@pytest.fixture(autouse=True)
def reset_foundation():
    """Reset Foundation state before each test."""
    reset_foundation_setup_for_testing()

Why this matters: - Foundation maintains global state (logger, hub, config) - Tests can interfere with each other without reset - Ensures clean state for each test

Capturing Logs in Tests

from provide.testkit import set_log_stream_for_testing
from io import StringIO

def test_logging_output():
    """Test log messages."""
    log_stream = StringIO()
    set_log_stream_for_testing(log_stream)

    from provide.foundation import logger
    logger.info("test message", value=42)

    output = log_stream.getvalue()
    assert "test message" in output
    assert "42" in output

Testing Hub Components

from provide.foundation.hub import get_hub

def test_hub_component():
    """Test Hub initialization."""
    hub = get_hub()
    hub.initialize_foundation()

    # Test hub functionality
    assert hub.is_initialized

Async Testing Patterns

Basic Async Tests

import pytest

@pytest.mark.asyncio
async def test_async_operation():
    """Test async function."""
    result = await my_async_function()
    assert result is not None

Async Fixtures

@pytest.fixture
async def async_client():
    """Provide async client."""
    client = AsyncClient()
    await client.connect()
    yield client
    await client.disconnect()

@pytest.mark.asyncio
async def test_with_async_client(async_client):
    """Test using async client."""
    result = await async_client.fetch_data()
    assert result

Testing Concurrent Operations

import asyncio
import pytest

@pytest.mark.asyncio
async def test_concurrent_operations():
    """Test multiple concurrent operations."""
    tasks = [
        async_operation(1),
        async_operation(2),
        async_operation(3)
    ]
    results = await asyncio.gather(*tasks)
    assert len(results) == 3
    assert all(r is not None for r in results)

Integration Testing

Cross-Component Tests

def test_integration_flow():
    """Test complete workflow."""
    # Setup
    config = load_config()
    manager = Manager(config)

    # Execute workflow
    result = manager.process_item(test_item)

    # Verify
    assert result.success
    assert result.output is not None

Testing with External Dependencies

import pytest
from unittest.mock import Mock, patch

@pytest.fixture
def mock_http_client():
    """Mock HTTP client."""
    with patch('httpx.Client') as mock:
        mock.return_value.get.return_value.json.return_value = {"status": "ok"}
        yield mock

def test_with_mock_http(mock_http_client):
    """Test using mocked HTTP client."""
    result = function_that_uses_http()
    assert result["status"] == "ok"

File Operation Testing

Testing File Operations

from pathlib import Path

def test_file_operations(tmp_path):
    """Test file read/write."""
    test_file = tmp_path / "test.txt"

    # Write
    test_file.write_text("test content")

    # Read
    content = test_file.read_text()
    assert content == "test content"

Testing Atomic Writes

from provide.foundation.file.atomic import atomic_write

def test_atomic_write(tmp_path):
    """Test atomic file writing."""
    target = tmp_path / "output.txt"

    with atomic_write(target) as f:
        f.write("atomic content")

    assert target.exists()
    assert target.read_text() == "atomic content"

Error Testing

Testing Expected Exceptions

import pytest
from my_module import MyError

def test_raises_error():
    """Test that error is raised."""
    with pytest.raises(MyError) as exc_info:
        function_that_raises()

    assert "expected message" in str(exc_info.value)

Testing Error Messages

def test_error_message():
    """Test error message content."""
    try:
        function_that_fails()
        pytest.fail("Expected exception was not raised")
    except ValueError as e:
        assert "specific error" in str(e)
        assert e.args[0] == "expected argument"

Performance Testing

Testing with Timeouts

import pytest

@pytest.mark.timeout(5)
def test_completes_quickly():
    """Test completes within 5 seconds."""
    slow_operation()

Benchmarking

import pytest

@pytest.mark.benchmark
def test_performance(benchmark):
    """Benchmark function performance."""
    result = benchmark(expensive_function, arg1, arg2)
    assert result is not None

Parametrized Testing

Basic Parametrization

import pytest

@pytest.mark.parametrize("input,expected", [
    ("hello", "HELLO"),
    ("world", "WORLD"),
    ("test", "TEST"),
])
def test_upper(input, expected):
    """Test uppercase conversion."""
    assert input.upper() == expected

Complex Parametrization

@pytest.mark.parametrize("config", [
    {"mode": "sync", "timeout": 30},
    {"mode": "async", "timeout": 60},
])
def test_with_config(config):
    """Test with different configurations."""
    result = process_with_config(config)
    assert result.mode == config["mode"]

Project-Specific Patterns

FlavorPack Testing

Use pretaster/taster for PSPF validation:

# Run PSPF validation tests
make validate-pspf

# Test specific builder/launcher combo
make validate-pspf-combo

Never create standalone test packages:

# โŒ DON'T: Create test packages in /tmp
test_package = create_package("/tmp/test.psp")

# โœ… DO: Use pretaster
from tests.pretaster import validate_package
validate_package(package_spec)

Pyvider RPC Plugin Testing

Always reset Foundation:

import pytest
from provide.testkit import reset_foundation_setup_for_testing

@pytest.fixture(autouse=True)
def reset_foundation():
    """Reset Foundation state."""
    reset_foundation_setup_for_testing()

@pytest.mark.asyncio
async def test_rpc_server():
    """Test RPC server."""
    server = plugin_server(protocol, handler)
    # Test server functionality

TofuSoup Conformance Testing

Run via CLI or pytest:

# Via soup CLI
soup test cty
soup test hcl
soup test rpc

# Via pytest
uv run pytest conformance/cty/ -v
uv run pytest -m cty

Test harness integration:

def test_with_harness():
    """Test using Go harness."""
    result = run_harness_command(["cty", "view", "test.json"])
    assert result.returncode == 0

SupSrc Event Testing

Complete event sequences required:

import asyncio

async def test_atomic_detection():
    """Test atomic save detection."""
    buffer = EventBuffer(mode="smart")

    # Send complete sequence
    buffer.add_event(create_event)
    buffer.add_event(modify_event)
    buffer.add_event(move_event)

    # Wait for detection window + post-delay + margin
    await asyncio.sleep(0.15)

    # Flush and verify
    groups = buffer.flush_all()
    assert len(groups) == 1

Test Markers

Standard Markers

@pytest.mark.slow          # Long-running tests
@pytest.mark.integration   # Integration tests
@pytest.mark.unit          # Unit tests
@pytest.mark.asyncio       # Async tests
@pytest.mark.benchmark     # Performance benchmarks

Running by Marker

# Skip slow tests
uv run pytest -m "not slow"

# Only integration tests
uv run pytest -m integration

# Unit tests excluding slow ones
uv run pytest -m "unit and not slow"

Coverage Best Practices

Measuring Coverage

# Generate coverage report
uv run pytest --cov=PACKAGE --cov-report=term-missing

# HTML report for detailed view
uv run pytest --cov=PACKAGE --cov-report=html
open htmlcov/index.html

# Fail if coverage below threshold
uv run pytest --cov=PACKAGE --cov-fail-under=80

Coverage Configuration

In pyproject.toml:

[tool.coverage.run]
source = ["src"]
omit = [
    "*/tests/*",
    "*/__pycache__/*",
    "*/site-packages/*",
]

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
    "if __name__ == .__main__.:",
    "if TYPE_CHECKING:",
]

Testing Checklist

Before pushing code, ensure:

  • All tests pass locally
  • New features have tests
  • Bug fixes have regression tests
  • Coverage doesn't decrease
  • Tests are fast (use markers for slow tests)
  • Async tests use proper fixtures
  • Foundation reset fixture used where needed
  • No test pollution (each test independent)
  • Clear test names and docstrings

Troubleshooting Tests

Tests Pass Individually, Fail Together

Cause: Shared state not being reset

Solution:

@pytest.fixture(autouse=True)
def reset_state():
    """Reset state before each test."""
    global_state.clear()
    yield
    global_state.clear()

Async Tests Hang

Cause: Unclosed resources or infinite loops

Solution:

@pytest.mark.timeout(10)
@pytest.mark.asyncio
async def test_with_timeout():
    """Test with timeout to prevent hanging."""
    await operation_that_might_hang()

Flaky Tests

Cause: Race conditions or timing issues

Solution:

import asyncio

async def test_with_retry():
    """Test with retry for flaky operations."""
    for _ in range(3):
        try:
            result = await flaky_operation()
            assert result
            break
        except AssertionError:
            await asyncio.sleep(0.1)
    else:
        pytest.fail("Test failed after 3 retries")

Additional Resources


Related Guides: - Development Workflow Guide - Daily development patterns - IDE Setup Guide - Configure testing in your IDE