Testing Strategies: The Pyramid That Actually Works
The testing pyramid isn't just theory. Here's how to structure tests that catch bugs without slowing you down.
February 24, 2026 · 7 min · 1367 words · Rob Washington
Table of Contents
Everyone agrees testing is important. Few agree on how much, what kind, and where to focus. The testing pyramid provides a framework: many fast unit tests, fewer integration tests, even fewer end-to-end tests.
But the pyramid only works if you understand why each layer exists.
Unit tests: Test individual functions/classes in isolation
Integration tests: Test components working together
E2E tests: Test the full system like a user would
fromunittest.mockimportMock,patchdeftest_send_welcome_email():# Mock the email servicemock_email=Mock()user_service=UserService(email_service=mock_email)user_service.create_user("alice@example.com")# Verify email was sentmock_email.send.assert_called_once_with(to="alice@example.com",template="welcome")@patch('services.payment_gateway')deftest_process_payment(mock_gateway):mock_gateway.charge.return_value={"id":"ch_123","status":"success"}result=process_order(order)assertresult.charge_id=="ch_123"mock_gateway.charge.assert_called_once()
# tests/integration/test_user_db.pyimportpytestfromdatabaseimportDatabasefromrepositoriesimportUserRepository@pytest.fixturedefdb():db=Database("postgresql://localhost/test_db")db.migrate()yielddbdb.clear()@pytest.fixturedefuser_repo(db):returnUserRepository(db)deftest_create_and_retrieve_user(user_repo):# Test actual database operationsuser=user_repo.create(email="alice@example.com",name="Alice")retrieved=user_repo.get(user.id)assertretrieved.email=="alice@example.com"assertretrieved.name=="Alice"deftest_find_by_email(user_repo):user_repo.create(email="bob@example.com",name="Bob")found=user_repo.find_by_email("bob@example.com")assertfoundisnotNoneassertfound.name=="Bob"
# tests/e2e/test_checkout.pyimportpytestfromplaywright.sync_apiimportPagedeftest_complete_checkout_flow(page:Page):# Navigate to productpage.goto("/products/widget")# Add to cartpage.click("button:has-text('Add to Cart')")# Go to checkoutpage.click("a:has-text('Checkout')")# Fill shipping infopage.fill("#email","test@example.com")page.fill("#address","123 Test St")page.fill("#city","Test City")# Enter paymentpage.fill("#card-number","4242424242424242")page.fill("#expiry","12/25")page.fill("#cvc","123")# Submit orderpage.click("button:has-text('Place Order')")# Verify successassertpage.locator("h1:has-text('Order Confirmed')").is_visible()assertpage.locator(".order-number").is_visible()
E2E test guidelines:
Test critical user journeys only
Keep them minimal (they’re slow and flaky)
Run in CI, not on every commit
Use stable selectors (data-testid, not CSS classes)
# Run unit tests constantly (< 10 seconds)pytest tests/unit/ -q
# Run integration tests before commit (< 2 minutes)pytest tests/integration/
# Run E2E tests in CI only (< 10 minutes)pytest tests/e2e/
Slow tests don’t get run. Fast tests catch bugs early.
# ❌ Bad: E2E test for validation logicdeftest_email_validation_in_signup(page):page.goto("/signup")page.fill("#email","invalid")page.click("button:submit")assertpage.locator(".error").text=="Invalid email"# ✅ Good: Unit test for validationdeftest_email_validation():assertvalidate_email("invalid")==Falseassertvalidate_email("valid@example.com")==True
The testing pyramid works because each layer serves a purpose. Unit tests verify logic quickly. Integration tests verify contracts. E2E tests verify user journeys.
Write many fast unit tests. Write enough integration tests to verify boundaries. Write few E2E tests for critical paths. Run the fast tests constantly, the slow tests before merge.
Tests that don’t run don’t help. Speed and reliability matter as much as coverage.
📬 Get the Newsletter
Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.