# Testing Anti-Patterns: Common Mistakes to Avoid ## Testing Implementation Details **The Anti-Pattern**: Tests verify internal mechanics rather than observable behavior, causing tests to break during safe refactoring. ### Examples of Testing Implementation Details **Testing Private Methods Directly** ```python # ❌ BAD: Testing private method class OrderService: def create_order(self, items): self._validate_items(items) return self._build_order(items) def _validate_items(self, items): # Internal implementation detail pass def _build_order(self, items): # Internal implementation detail pass def test_validate_items_rejects_empty(): service = OrderService() # Accessing private method with pytest.raises(ValidationError): service._validate_items([]) ``` **Testing Call Sequences** ```python # ❌ BAD: Verifying internal method calls def test_order_creation_process(): mock_validator = Mock() mock_builder = Mock() service = OrderService(mock_validator, mock_builder) service.create_order(items) # Testing HOW it works internally mock_validator.validate.assert_called_once_with(items) mock_builder.build.assert_called_once() assert mock_validator.validate.call_count == 1 ``` **Testing Internal State** ```python # ❌ BAD: Accessing private fields def test_user_activation(): user = User() user.activate() assert user._activated_at is not None # Private field assert user._activation_status == "active" # Private field ``` ### How to Fix It **Test Through Public Interfaces** ```python # ✅ GOOD: Testing observable behavior def test_create_order_with_valid_items(): service = OrderService() items = [Item("Widget", 10)] order = service.create_order(items) assert order.id is not None assert order.total == 10 assert len(order.items) == 1 def test_create_order_with_empty_items_raises_error(): service = OrderService() with pytest.raises(ValidationError): service.create_order([]) ``` **Test Outcomes, Not Process** ```python # ✅ GOOD: Verifying results def test_order_completion(): order_service = OrderService(email_service) completed_order = order_service.complete_order(order_id=123) assert completed_order.status == "completed" assert completed_order.completed_at is not None # Email sending is internal detail - verify user-visible outcome ``` ### Why It Matters - **Refactoring breaks tests**: Changing internal structure breaks tests even though behavior is unchanged - **False negatives**: Tests fail when nothing is actually broken - **Maintenance burden**: Every internal change requires test updates - **Missed real bugs**: Focus on implementation means less focus on behavior - **Discourages improvement**: Fear of breaking tests prevents refactoring --- ## Over-Mocking and Excessive Test Doubles **The Anti-Pattern**: Mocking every dependency regardless of whether it's necessary, creating tests that pass while real code fails. ### Examples of Over-Mocking **Mocking Everything** ```python # ❌ BAD: Mocking internal collaborators def test_user_service(): mock_validator = Mock() mock_repository = Mock() mock_email_service = Mock() mock_logger = Mock() mock_cache = Mock() mock_event_bus = Mock() service = UserService( mock_validator, mock_repository, mock_email_service, mock_logger, mock_cache, mock_event_bus ) # Test provides no confidence real code works service.create_user(data) # Verifying every interaction mock_validator.validate.assert_called() mock_repository.save.assert_called() # ... and so on ``` **Mocking What You Own** ```python # ❌ BAD: Mocking internal business logic def test_order_total_calculation(): mock_calculator = Mock() mock_calculator.calculate.return_value = 100 order_service = OrderService(mock_calculator) total = order_service.get_order_total(order) assert total == 100 # This test verifies nothing - just that mock returns what we told it to ``` ### How to Fix It **Mock Only at System Boundaries** ```python # ✅ GOOD: Mock external dependencies, use real internal objects def test_user_service(): # Mock external system mock_email_service = Mock() # Real internal collaborators validator = UserValidator() repository = InMemoryUserRepository() service = UserService(validator, repository, mock_email_service) user = service.create_user(valid_user_data) assert user.id is not None assert repository.find(user.id) == user ``` **Use Real Implementations Where Possible** ```python # ✅ GOOD: Real implementations for testable components def test_order_total_calculation(): calculator = OrderCalculator() # Real calculator order = Order(items=[Item("Widget", 10), Item("Gadget", 20)]) total = calculator.calculate_total(order) assert total == 30 ``` ### When to Mock **DO mock**: - External HTTP APIs - Databases (sometimes - consider in-memory alternatives) - File systems - System time/clocks - Random number generators - Third-party services **DON'T mock**: - Internal business logic - Value objects and data structures - Utilities and helpers you own - Simple collaborators ### Why It Matters - **False confidence**: Tests pass but real integrations fail - **Implementation coupling**: Tests break when changing internal code structure - **Maintenance nightmare**: Every refactoring breaks mocks - **Missing integration bugs**: Real interactions between components never tested - **Complex test setup**: More time setting up mocks than writing actual test --- ## Brittle Tests That Block Refactoring **The Anti-Pattern**: Tests so tightly coupled to current implementation that refactoring becomes impossible without rewriting tests. ### Examples of Brittle Tests **UI Tests Coupled to Selectors** ```python # ❌ BAD: Coupled to specific HTML structure def test_login(): browser.find_element_by_xpath( '/html/body/div[1]/div[2]/form/div[1]/input' ).send_keys('user@example.com') browser.find_element_by_xpath( '/html/body/div[1]/div[2]/form/div[2]/input' ).send_keys('password') browser.find_element_by_xpath( '/html/body/div[1]/div[2]/form/button[1]' ).click() ``` **Tests Dependent on Execution Order** ```python # ❌ BAD: Tests must run in specific order def test_1_create_user(): global user_id user_id = create_user("test@example.com") def test_2_activate_user(): activate_user(user_id) # Depends on test_1 def test_3_delete_user(): delete_user(user_id) # Depends on test_2 ``` **Tests Coupled to Data Format** ```python # ❌ BAD: Expects exact JSON structure def test_api_response(): response = api.get_user(123) assert response == { 'id': 123, 'name': 'Test User', 'email': 'test@example.com', 'created_at': '2024-01-01T00:00:00Z', 'updated_at': '2024-01-01T00:00:00Z' } # Breaks if any field added or order changes ``` ### How to Fix It **Use Stable Selectors** ```python # ✅ GOOD: Use semantic selectors def test_login(): browser.find_element_by_label_text('Email').send_keys('user@example.com') browser.find_element_by_label_text('Password').send_keys('password') browser.find_element_by_role('button', name='Login').click() # Or use data-testid attributes def test_login_with_test_ids(): browser.find_element_by_test_id('email-input').send_keys('user@example.com') browser.find_element_by_test_id('password-input').send_keys('password') browser.find_element_by_test_id('login-button').click() ``` **Make Tests Independent** ```python # ✅ GOOD: Each test is self-contained def test_create_user(): user_id = create_user("test@example.com") assert user_id is not None def test_activate_user(): user_id = create_user("test2@example.com") result = activate_user(user_id) assert result.success def test_delete_user(): user_id = create_user("test3@example.com") delete_user(user_id) assert find_user(user_id) is None ``` **Test Required Fields, Ignore Optional Ones** ```python # ✅ GOOD: Test essential fields only def test_api_response(): response = api.get_user(123) assert response['id'] == 123 assert response['email'] == 'test@example.com' assert 'created_at' in response # Don't care about other fields or order ``` ### Why It Matters - **Prevents refactoring**: Fear of breaking tests stops code improvement - **High maintenance cost**: Every small change requires test updates - **False failures**: Tests fail when nothing is actually broken - **Slows development**: More time fixing tests than improving code - **Tests become liability**: Team considers tests obstacle rather than asset --- ## The Inverted Test Pyramid (Ice Cream Cone) **The Anti-Pattern**: More E2E and integration tests than unit tests, leading to slow, flaky test suites. ### What It Looks Like ``` /\ / \ ← Large E2E test suite (slow, flaky) / \ /------\ ← Moderate integration tests / \ / unit \ ← Small unit test suite /____________\ ``` **Symptoms**: - Test suite takes 30+ minutes to run - Frequent flaky test failures - Developers skip running tests locally - CI pipeline is bottleneck - Hard to diagnose failures ### How It Happens **Over-Reliance on E2E Tests** ```python # ❌ BAD: Testing edge cases through UI def test_registration_empty_email(): browser.get('/register') # ... fill form with empty email ... # 10 seconds to test something unit test could verify in milliseconds def test_registration_invalid_email(): browser.get('/register') # ... fill form with invalid email ... # Another 10 seconds def test_registration_duplicate_email(): browser.get('/register') # ... fill form with existing email ... # Another 10 seconds # 30 seconds to test what should be 3 fast unit tests ``` **Missing Unit Tests** ```python # ❌ BAD: No unit tests for business logic # All testing happens through API or UI # Business logic never tested in isolation ``` ### How to Fix It **Push Tests Down the Pyramid** ```python # ✅ GOOD: Unit test edge cases def test_email_validation_empty(): with pytest.raises(ValidationError): validate_email("") def test_email_validation_invalid_format(): with pytest.raises(ValidationError): validate_email("notanemail") def test_email_validation_duplicate(): with pytest.raises(ValidationError): validate_email_uniqueness("existing@example.com") # Each test: milliseconds # ✅ GOOD: One E2E test for happy path def test_successful_registration(): browser.get('/register') fill_registration_form(valid_data) submit() assert_redirected_to_dashboard() # One E2E test: 10 seconds ``` **Proper Test Distribution** ``` /\ /E2E\ ← Few critical path tests (5-10%) /____\ / \ / Int \ ← Component boundary tests (15-25%) / Tests \ /____________\ / \ / Unit Tests \ ← Most tests here (70-80%) /__________________\ ``` ### Why It Matters - **Slow feedback**: 30-minute test suites kill productivity - **Flaky tests**: E2E tests are inherently less stable - **Unclear failures**: Hard to pinpoint bugs in complex E2E tests - **Resource intensive**: E2E tests require more infrastructure - **Maintenance burden**: UI changes break many E2E tests --- ## Assertion Roulette and Unclear Failures **The Anti-Pattern**: Tests with multiple unrelated assertions that make failures ambiguous. ### Examples **Too Many Assertions** ```python # ❌ BAD: Which assertion failed? def test_user_operations(): user = create_user("test@example.com") assert user.id is not None assert user.email == "test@example.com" assert user.is_active user.update(name="Test User") assert user.name == "Test User" user.deactivate() assert not user.is_active user.delete() assert find_user(user.id) is None # If test fails on line 7, was it deactivate() or the assertion that broke? ``` **No Assertion Messages** ```python # ❌ BAD: Failure gives no context def test_discount_calculation(): result = calculate_discount(customer, order) assert result == 10 # Failure: AssertionError: assert 0 == 10 # (Why did we expect 10? What were the inputs?) ``` ### How to Fix It **One Logical Assertion Per Test** ```python # ✅ GOOD: Focused tests def test_user_creation_assigns_id(): user = create_user("test@example.com") assert user.id is not None def test_user_creation_sets_email(): user = create_user("test@example.com") assert user.email == "test@example.com" def test_new_user_is_active_by_default(): user = create_user("test@example.com") assert user.is_active ``` **Add Descriptive Assertion Messages** ```python # ✅ GOOD: Clear failure messages def test_vip_discount_calculation(): customer = Customer(type="VIP") order = Order(total=100) discount = calculate_discount(customer, order) assert discount == 10, \ f"VIP customer with ${order.total} order should get $10 discount, got ${discount}" ``` ### Why It Matters - **Unclear failures**: Can't tell what broke without debugging - **Wasted time**: Must debug to understand test failure - **Cascading failures**: One bug causes multiple assertion failures - **Hard to maintain**: Complex tests are hard to understand and update --- ## Global State and Hidden Dependencies **The Anti-Pattern**: Tests that depend on global state, singletons, or hidden dependencies, creating order dependencies and mysterious failures. ### Examples **Global State** ```python # ❌ BAD: Tests share global state current_user = None def test_login(): global current_user current_user = login("user@example.com", "password") assert current_user is not None def test_access_dashboard(): # Depends on global current_user from previous test response = get_dashboard(current_user) assert response.status == 200 ``` **Singleton Dependencies** ```python # ❌ BAD: Singleton holds state between tests class Database: _instance = None _data = {} @classmethod def instance(cls): if cls._instance is None: cls._instance = cls() return cls._instance def test_save_user(): db = Database.instance() db.save(user) # Pollutes singleton state def test_user_count(): db = Database.instance() # Still has data from previous test assert db.count() == 1 # Breaks if other tests ran first ``` ### How to Fix It **Explicit Dependencies** ```python # ✅ GOOD: Explicit fixtures @pytest.fixture def authenticated_user(): user = User(email="test@example.com") session = login(user) return session def test_access_dashboard(authenticated_user): response = get_dashboard(authenticated_user) assert response.status == 200 ``` **Dependency Injection** ```python # ✅ GOOD: Inject dependencies @pytest.fixture def database(): db = Database() yield db db.clear() def test_save_user(database): user = User(email="test@example.com") database.save(user) assert database.count() == 1 def test_find_user(database): user = User(email="test@example.com") database.save(user) found = database.find_by_email("test@example.com") assert found == user ``` ### Why It Matters - **Order dependencies**: Tests must run in specific order - **Flaky failures**: Tests pass individually but fail in suite - **Parallel execution impossible**: Can't run tests concurrently - **Hard to debug**: Failures depend on what ran before - **Misleading results**: Test results inconsistent between runs --- ## High Coverage, Low Value Tests **The Anti-Pattern**: Achieving high code coverage with tests that don't catch bugs, providing false sense of security. ### Examples **Tests That Don't Assert** ```python # ❌ BAD: Executes code but verifies nothing def test_process_order(): service = OrderService() service.process(order) # No assertions - just executes code for coverage ``` **Testing Trivial Code** ```python # ❌ BAD: Testing getters and setters def test_user_get_email(): user = User(email="test@example.com") assert user.get_email() == "test@example.com" def test_user_set_email(): user = User() user.set_email("test@example.com") assert user.email == "test@example.com" ``` **Testing Framework Behavior** ```python # ❌ BAD: Testing that framework works def test_django_model_save(): user = User(email="test@example.com") user.save() # Just testing Django's save() works - not your code ``` ### How to Fix It **Test Behavior, Not Code Execution** ```python # ✅ GOOD: Verifies actual behavior def test_order_processing_updates_inventory(): inventory = Inventory(product_id=1, quantity=10) order = Order(product_id=1, quantity=3) process_order(order, inventory) assert inventory.quantity == 7 def test_order_processing_sends_confirmation(): mock_email = Mock() order = Order(customer_email="test@example.com") process_order(order, email_service=mock_email) mock_email.send_confirmation.assert_called_with("test@example.com") ``` **Focus on Complex Logic** ```python # ✅ GOOD: Test non-trivial behavior def test_discount_calculation_for_bulk_orders(): items = [Item("Widget", 10, quantity=100)] discount = calculate_bulk_discount(items) assert discount == 500 # 50% off for orders > 50 units ``` ### Why It Matters - **False confidence**: High coverage numbers hide lack of actual testing - **Bugs slip through**: Tests don't catch real problems - **Wasted effort**: Time spent on tests that provide no value - **Misleading metrics**: Coverage metric becomes meaningless --- ## Summary: Avoiding Anti-Patterns | Anti-Pattern | Impact | Solution | |-------------|---------|----------| | Testing implementation details | Brittle tests, blocks refactoring | Test through public interfaces, verify behavior | | Over-mocking | False confidence, missed integration bugs | Mock only external boundaries | | Brittle tests | High maintenance, prevents improvement | Use stable selectors, test essentials | | Inverted pyramid | Slow, flaky suite | Push tests down: more unit, fewer E2E | | Assertion roulette | Unclear failures | One logical assertion per test | | Global state | Order dependencies, flaky tests | Use fixtures, dependency injection | | Coverage without value | False security | Test behavior, not code execution | **Core Principle**: Good tests enable confident refactoring and catch real bugs. If tests block improvement or fail without catching bugs, they've become an anti-pattern.