Skip to content

Testing Best Practices

Testing best practices provide guidelines and patterns for effective testing with dependency injection.

๐ŸŽฏ Testing Principles

Test Behavior, Not Implementation

# โœ… Good: Test what the service does
def test_user_registration_sends_welcome_email(container):
    """Test that user registration sends welcome email."""
    mock_email = MockEmailService()
    container.bind(IEmailService, mock_email)

    user_service = container.get(IUserService)
    user = user_service.register_user("john@example.com", "password")

    # Verify behavior: email is sent
    assert mock_email.call_count("send_welcome_email") == 1
    assert user.email == "john@example.com"

# โŒ Bad: Test how the service does it
def test_user_registration_calls_internal_methods(container):
    """Test internal implementation details."""
    user_service = container.get(IUserService)

    user_service.register_user("john@example.com", "password")

    # Testing implementation: private method calls
    assert user_service._validate_email_called
    assert user_service._hash_password_called
    assert user_service._save_user_called

Test Public Interfaces

# โœ… Good: Test public API
def test_price_calculation_public_api(container):
    """Test price calculation through public interface."""
    calculator = container.get(IPriceCalculator)

    order = Order(items=[Item(price=10, quantity=2), Item(price=5, quantity=1)])
    total = calculator.calculate_total(order)

    assert total == 25  # (10*2) + (5*1)

# โŒ Bad: Test private methods
def test_price_calculation_private_methods(container):
    """Test private implementation details."""
    calculator = container.get(IPriceCalculator)

    # Don't test private methods
    tax = calculator._calculate_tax(100)
    discount = calculator._apply_discount(100, 10)

    assert tax == 8.0
    assert discount == 90.0

๐Ÿ”ง Test Organization

Test File Structure

# tests/
# โ”œโ”€โ”€ unit/
# โ”‚   โ”œโ”€โ”€ test_user_service.py
# โ”‚   โ”œโ”€โ”€ test_order_service.py
# โ”‚   โ””โ”€โ”€ test_price_calculator.py
# โ”œโ”€โ”€ integration/
# โ”‚   โ”œโ”€โ”€ test_user_registration_flow.py
# โ”‚   โ”œโ”€โ”€ test_order_processing_flow.py
# โ”‚   โ””โ”€โ”€ test_payment_flow.py
# โ”œโ”€โ”€ e2e/
# โ”‚   โ”œโ”€โ”€ test_complete_user_journey.py
# โ”‚   โ””โ”€โ”€ test_admin_workflow.py
# โ””โ”€โ”€ fixtures/
#     โ”œโ”€โ”€ conftest.py
#     โ”œโ”€โ”€ test_data.py
#     โ””โ”€โ”€ mock_services.py

Test Naming Conventions

# โœ… Good: Descriptive test names
def test_user_registration_with_valid_email_sends_welcome_email():
    pass

def test_order_calculation_applies_tax_correctly():
    pass

def test_payment_processing_handles_card_decline_gracefully():
    pass

# โŒ Bad: Vague test names
def test_user():
    pass

def test_calculation():
    pass

def test_payment():
    pass

# โœ… Good: Test method naming patterns
def test_[unit_of_work]_[scenario]_[expected_result]():
    pass

# Examples:
def test_user_registration_valid_email_creates_account():
    pass

def test_order_total_calculation_includes_tax_and_discount():
    pass

def test_payment_gateway_timeout_retries_automatically():
    pass

Test Class Organization

class TestUserService:
    """Test cases for UserService."""

    def setup_method(self):
        """Setup for each test method."""
        self.container = TestContainer()
        # Setup common test data

    def test_successful_registration(self):
        """Test successful user registration."""
        pass

    def test_registration_with_duplicate_email_fails(self):
        """Test registration fails with duplicate email."""
        pass

    def test_password_reset_sends_email(self):
        """Test password reset sends email."""
        pass

class TestUserServiceIntegration:
    """Integration tests for UserService."""

    def test_registration_creates_user_in_database(self):
        """Test that registration persists user to database."""
        pass

    def test_registration_sends_welcome_email_via_real_service(self):
        """Test email sending with real email service."""
        pass

๐ŸŽจ Test Setup Patterns

Fixture Best Practices

# โœ… Good: Descriptive fixture names
@pytest.fixture
def user_service_with_mocked_dependencies():
    """User service with mocked email and database."""
    container = TestContainer()
    container.bind_mock(IEmailService)
    container.bind_mock(IUserRepository)

    service = container.get(IUserService)
    return service

@pytest.fixture
def integration_container():
    """Container for integration tests."""
    container = TestContainer()
    container.install(TestDatabaseModule())
    container.install(BusinessLogicModule())
    container.bind_mock(IExternalAPI)  # Only mock externals

    return container

# โŒ Bad: Generic fixture names
@pytest.fixture
def container():
    """Too generic, unclear purpose."""
    return TestContainer()

@pytest.fixture
def service():
    """Unclear which service."""
    container = TestContainer()
    return container.get(IService)

Test Data Management

# โœ… Good: Factory pattern for test data
class TestDataFactory:
    @staticmethod
    def create_user(email=None, name=None):
        return User(
            id=None,  # Will be set by database
            email=email or f"test_{uuid.uuid4().hex[:8]}@example.com",
            name=name or "Test User",
            created_at=datetime.now()
        )

    @staticmethod
    def create_order(user_id, items=None):
        if items is None:
            items = [TestDataFactory.create_order_item()]

        return Order(
            id=None,
            user_id=user_id,
            items=items,
            total=sum(item.price * item.quantity for item in items),
            status="pending"
        )

def test_order_creation_with_factory(container):
    """Test order creation using test data factory."""
    user = TestDataFactory.create_user()
    order = TestDataFactory.create_order(user.id)

    order_service = container.get(IOrderService)
    created_order = order_service.create_order(order)

    assert created_order.user_id == user.id
    assert created_order.status == "pending"

# โœ… Good: Builder pattern for complex objects
class UserBuilder:
    def __init__(self):
        self.email = f"test_{uuid.uuid4().hex[:8]}@example.com"
        self.name = "Test User"
        self.is_active = True
        self.roles = []

    def with_email(self, email):
        self.email = email
        return self

    def with_name(self, name):
        self.name = name
        return self

    def inactive(self):
        self.is_active = False
        return self

    def with_roles(self, *roles):
        self.roles.extend(roles)
        return self

    def build(self):
        return User(
            id=None,
            email=self.email,
            name=self.name,
            is_active=self.is_active,
            roles=self.roles,
            created_at=datetime.now()
        )

def test_user_with_builder(container):
    """Test user creation using builder pattern."""
    user = (UserBuilder()
            .with_email("john@example.com")
            .with_name("John Doe")
            .with_roles("admin", "user")
            .build())

    user_service = container.get(IUserService)
    created_user = user_service.create_user(user)

    assert created_user.email == "john@example.com"
    assert "admin" in created_user.roles

๐Ÿงช Mock and Stub Patterns

Appropriate Mock Usage

# โœ… Good: Mock external dependencies
def test_user_service_with_mocked_email(container):
    """Mock external email service."""
    mock_email = container.bind_mock(IEmailService)

    user_service = container.get(IUserService)
    user = user_service.register_user("john@example.com", "password")

    # Verify external service was called
    assert mock_email.call_count("send_welcome_email") == 1

# โœ… Good: Mock slow or unreliable services
def test_payment_with_mocked_gateway(container):
    """Mock external payment gateway."""
    mock_payment = container.bind_mock(IPaymentGateway)
    mock_payment.configure_return("charge_card", "txn_123")

    payment_service = container.get(IPaymentService)
    result = payment_service.process_payment(100, "card_token")

    assert result.success is True
    assert result.transaction_id == "txn_123"

# โŒ Bad: Mock business logic
def test_business_logic_with_mocked_calculator(container):
    """Don't mock business logic you want to test."""
    mock_calculator = container.bind_mock(IPriceCalculator)
    mock_calculator.configure_return("calculate_total", 100)

    # This test is meaningless - just testing the mock
    order_service = container.get(IOrderService)
    total = order_service.calculate_order_total(order)

    assert total == 100  # Just verifying mock behavior

Stub vs Mock Distinction

# โœ… Use stubs for data provision
def test_with_stub_data(container):
    """Use stub for predictable test data."""
    stub_repo = StubUserRepository()
    stub_repo.save_user(TestDataFactory.create_user("existing@example.com"))

    container.bind(IUserRepository, stub_repo)

    user_service = container.get(IUserService)

    # Test with predictable existing data
    with pytest.raises(EmailExistsError):
        user_service.register_user("existing@example.com", "password")

# โœ… Use mocks for interaction verification
def test_with_mock_verification(container):
    """Use mock to verify interactions."""
    mock_email = MockEmailService()
    container.bind(IEmailService, mock_email)

    user_service = container.get(IUserService)
    user_service.register_user("john@example.com", "password")

    # Verify the interaction occurred
    assert mock_email.call_count("send_welcome_email") == 1
    assert mock_email.was_called_with("send_welcome_email", "john@example.com")

๐Ÿšจ Common Testing Mistakes

1. Testing Implementation Details

# โŒ Bad: Testing private methods
def test_private_method(container):
    service = container.get(IService)
    result = service._private_method()  # Don't test private methods
    assert result == expected

# โŒ Bad: Testing internal state
def test_internal_state(container):
    service = container.get(IService)
    service.do_something()

    # Don't test internal state
    assert service._internal_counter == 1
    assert len(service._internal_list) == 5

# โœ… Good: Test observable behavior
def test_observable_behavior(container):
    service = container.get(IService)
    result = service.do_something()

    # Test what the service does, not how
    assert result.success is True
    assert result.data is not None

2. Over-Mocking

# โŒ Bad: Mocking everything
def test_with_too_many_mocks(container):
    container.bind_mock(IUserService)
    container.bind_mock(IEmailService)
    container.bind_mock(IValidator)
    container.bind_mock(IPasswordHasher)
    container.bind_mock(ILogger)

    # Test becomes meaningless
    service = container.get(IService)
    result = service.do_work()
    assert result is not None

# โœ… Good: Mock only what's necessary
def test_with_appropriate_mocking(container):
    # Mock external dependencies
    container.bind_mock(IEmailService)      # External API
    container.bind_mock(IPaymentGateway)    # External service

    # Use real implementations for business logic
    container.bind(IUserService, UserService())
    container.bind(IValidator, EmailValidator())

    # Test focuses on real business logic
    service = container.get(IService)
    result = service.do_work()
    assert result.success is True

3. No Test Isolation

# โŒ Bad: Shared state between tests
class TestWithSharedState:
    container = TestContainer()  # Shared instance!

    def test1(self):
        user = self.container.get(IUserService).create_user("test@example.com")
        assert user.email == "test@example.com"

    def test2(self):
        # Sees user from test1!
        users = self.container.get(IUserService).get_all_users()
        assert len(users) == 1  # Unexpected!

# โœ… Good: Isolated tests
class TestWithIsolation:
    def setup_method(self):
        self.container = TestContainer()  # Fresh instance per test

    def test1(self):
        user = self.container.get(IUserService).create_user("test@example.com")
        assert user.email == "test@example.com"

    def test2(self):
        # Fresh container, no data from test1
        users = self.container.get(IUserService).get_all_users()
        assert len(users) == 0  # Expected

4. Ignoring Test Performance

# โŒ Bad: Slow tests
def test_slow_operation(container):
    # No timeout or performance consideration
    service = container.get(IService)
    result = service.slow_operation()  # Takes 30 seconds
    assert result is not None

# โœ… Good: Performance-aware tests
def test_operation_within_timeout(container):
    service = container.get(IService)

    import time
    start = time.time()
    result = service.do_operation()
    duration = time.time() - start

    assert result is not None
    assert duration < 5.0  # Should complete within 5 seconds

5. Inconsistent Test Data

# โŒ Bad: Hardcoded test data
def test_with_hardcoded_data(container):
    user_service = container.get(IUserService)

    # Hardcoded data causes conflicts
    user = user_service.create_user("test@example.com", "password")
    assert user.email == "test@example.com"

def test_another_with_hardcoded_data(container):
    user_service = container.get(IUserService)

    # Same hardcoded data causes conflicts
    user = user_service.create_user("test@example.com", "password")
    # This test might fail if previous test ran

# โœ… Good: Unique test data
def test_with_unique_data(container):
    user_service = container.get(IUserService)

    # Unique data per test
    email = f"test_{uuid.uuid4().hex[:8]}@example.com"
    user = user_service.create_user(email, "password")
    assert user.email == email

๐Ÿ“Š Test Quality Metrics

Test Coverage Guidelines

# pytest configuration for coverage
# pyproject.toml
[tool.coverage.run]
source = ["injectq"]
omit = [
    "*/tests/*",
    "*/migrations/*",
    "*/venv/*"
]

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

Coverage Targets

  • Unit Tests: 80%+ coverage of business logic
  • Integration Tests: Cover critical user journeys
  • Branches: 75%+ branch coverage
  • Lines: 85%+ line coverage

Test Performance Benchmarks

# conftest.py
def pytest_configure(config):
    config.addinivalue_line(
        "markers", "slow: marks tests as slow (deselect with '-m \"not slow\"')"
    )
    config.addinivalue_line(
        "markers", "integration: marks tests as integration tests"
    )

# Test with performance markers
@pytest.mark.slow
def test_slow_integration():
    """Mark slow tests."""
    pass

@pytest.mark.integration
def test_integration_test():
    """Mark integration tests."""
    pass

โœ… Testing Best Practices Checklist

Test Structure

  • Tests are organized by type (unit, integration, e2e)
  • Test names are descriptive and follow naming conventions
  • Tests use appropriate fixtures for setup
  • Test data is unique and isolated
  • Tests clean up after themselves

Test Quality

  • Tests focus on behavior, not implementation
  • Tests use appropriate mocking strategies
  • Tests cover both success and failure scenarios
  • Tests are fast and reliable
  • Tests provide clear failure messages

Test Maintenance

  • Tests are easy to understand and modify
  • Tests use factories/builders for complex data
  • Tests avoid shared state
  • Tests have appropriate performance expectations
  • Tests are regularly reviewed and updated

CI/CD Integration

  • Tests run in CI pipeline
  • Test coverage is tracked and reported
  • Performance regressions are caught
  • Tests run in parallel where possible
  • Test results are easily accessible

๐ŸŽฏ Summary

Effective testing with dependency injection requires:

  • Clear test organization - Separate unit, integration, and e2e tests
  • Appropriate mocking - Mock externals, test real business logic
  • Proper isolation - Each test gets fresh, unique data
  • Behavior focus - Test what services do, not implementation details
  • Performance awareness - Keep tests fast and monitor performance

Key principles: - Test public interfaces and observable behavior - Use fixtures for consistent test setup - Mock external dependencies, use real business logic - Ensure test isolation and cleanup - Write descriptive test names and clear assertions - Monitor test coverage and performance

Common patterns: - Factory pattern for test data creation - Builder pattern for complex objects - Fixture-based test setup - Appropriate mock vs stub usage - Performance benchmarking in tests

Quality checklist: - Tests are fast, isolated, and reliable - Test names are descriptive - Coverage meets targets - Tests focus on behavior over implementation - CI/CD integration is solid

This completes the testing section! Ready to explore advanced features?