Skip to content

Advanced Testing in Python

This guide covers advanced testing techniques for Python applications, focusing on challenging scenarios and best practices.

Table of Contents

Introduction

Testing Challenges

  • Concurrency and parallelism
  • Time-dependent behavior
  • Randomness and stochastic processes
  • Error handling and edge cases
  • External dependencies
  • File system interactions
  • Network conditions
  • Configuration variations

Testing Tools

  • pytest: Modern testing framework
  • unittest: Standard library testing
  • mock: Mocking library
  • freezegun: Time mocking
  • requests-mock: HTTP mocking
  • pytest-asyncio: Async testing

Test Organization

Test Structure

# test_example.py
import pytest

def test_basic_functionality():
    assert True

class TestClass:
    def test_method(self):
        assert True

    @pytest.mark.parametrize("input,expected", [
        (1, 2),
        (2, 4),
        (3, 6)
    ])
    def test_parameterized(self, input, expected):
        assert input * 2 == expected

Test Categories

@pytest.mark.unit
def test_unit():
    pass

@pytest.mark.integration
def test_integration():
    pass

@pytest.mark.e2e
def test_e2e():
    pass

Fixtures and Setup

Basic Fixtures

1
2
3
4
5
6
@pytest.fixture
def sample_data():
    return {"key": "value"}

def test_with_fixture(sample_data):
    assert sample_data["key"] == "value"

Fixture Scope

@pytest.fixture(scope="session")
def db_connection():
    # Setup
    connection = create_connection()
    yield connection
    # Teardown
    connection.close()

@pytest.fixture(scope="function")
def test_data():
    return {"id": 1}

Fixture Dependencies

1
2
3
4
5
6
7
@pytest.fixture
def user(db_connection):
    return create_user(db_connection)

@pytest.fixture
def user_with_profile(user):
    return add_profile(user)

Advanced Testing Scenarios

Concurrency Testing

import pytest
import threading

@pytest.fixture
def shared_counter():
    return {"value": 0}

def test_thread_safety(shared_counter):
    def increment():
        shared_counter["value"] += 1

    threads = [threading.Thread(target=increment) for _ in range(5)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    assert shared_counter["value"] == 5

Async Testing

import pytest
import asyncio

@pytest.mark.asyncio
async def test_async_operation():
    async def operation():
        await asyncio.sleep(0.1)
        return "result"

    result = await operation()
    assert result == "result"

Time-Dependent Testing

1
2
3
4
5
6
7
8
from freezegun import freeze_time
from datetime import datetime

@freeze_time("2025-01-01 12:00:00")
def test_time_dependent():
    now = datetime.now()
    assert now.year == 2025
    assert now.hour == 12

Mocking and Stubbing

Basic Mocking

1
2
3
4
5
6
from unittest.mock import Mock, patch

def test_mock_function():
    mock_func = Mock(return_value=42)
    assert mock_func() == 42
    mock_func.assert_called_once()

API Mocking

1
2
3
4
5
6
7
8
import requests
import requests_mock

def test_api_call():
    with requests_mock.Mocker() as mock:
        mock.get("https://api.example.com/data", json={"key": "value"})
        response = requests.get("https://api.example.com/data")
        assert response.json() == {"key": "value"}

Database Mocking

1
2
3
4
5
6
7
8
from unittest.mock import Mock

def test_database_operation():
    mock_db = Mock()
    mock_db.query.return_value = [{"id": 1, "name": "test"}]

    result = mock_db.query("SELECT * FROM users")
    assert result == [{"id": 1, "name": "test"}]

Performance Testing

Basic Performance Test

1
2
3
4
5
6
7
8
import time
import pytest

def test_performance():
    start_time = time.time()
    # Perform operation
    end_time = time.time()
    assert end_time - start_time < 1.0  # Should complete within 1 second

Memory Profiling

1
2
3
4
5
6
7
import pytest
from memory_profiler import profile

@profile
def test_memory_usage():
    # Perform memory-intensive operation
    pass

Load Testing

import pytest
import asyncio

@pytest.mark.asyncio
async def test_concurrent_requests():
    async def make_request():
        # Simulate request
        await asyncio.sleep(0.1)
        return "response"

    tasks = [make_request() for _ in range(100)]
    responses = await asyncio.gather(*tasks)
    assert len(responses) == 100

Best Practices

  1. Test Organization
  2. Use clear test names
  3. Group related tests
  4. Follow AAA pattern (Arrange, Act, Assert)

  5. Test Isolation

  6. Each test should be independent
  7. Clean up resources properly
  8. Use appropriate fixtures

  9. Error Handling

  10. Test both success and failure cases
  11. Verify error messages
  12. Test edge cases

  13. Maintenance

  14. Keep tests simple and focused
  15. Document complex test scenarios
  16. Regular test maintenance

  17. Debugging

  18. Performance Optimization
  19. Error Handling
  20. Concurrency