JSTAcademy
0 XP
Dashboard
Software Engineering
Testing Strategies
14 min
Masters+150 XP
Software Engineering · Masters

Testing Strategies

Unit, integration, E2E — test pyramid, TDD, property-based testing
14 min read+150 XP on completionCert: Software Engineering Discipline
Tap any word in the text below to start reading from there.

Testing Strategies

Tests are the primary tool for building confidence that code behaves as intended. The question is not whether to test but how: which tests to write, at what granularity, and in what balance to maximize confidence per unit of maintenance cost.

The Test Pyramid

Mike Cohn's Test Pyramid describes the ideal distribution of test types:

Unit Tests (bottom, most): test individual functions or classes in isolation. Fast (milliseconds), deterministic, pinpoint failures precisely. Should comprise ~70% of tests.

Integration Tests (middle): test how components interact — a service with its database, an API with authentication middleware, a cache layer with its backing store. Slower (seconds), require infrastructure, but verify that components actually work together. ~20% of tests.

End-to-End Tests (top, fewest): simulate a real user interacting with the full system through the UI or API. Slowest (minutes), fragile (flaky), expensive to maintain, but test the actual user experience. ~10% of tests.

The Ice Cream Cone anti-pattern (inverted pyramid) has mostly E2E tests: slow, expensive, fragile, hard to diagnose. A single E2E failure might require hours to investigate.

Unit Testing Well

Good unit tests:

  • Test behavior, not implementation: test that calculateTax(100) returns 10, not that it called getTaxRate() exactly once. Implementation-testing creates brittle tests that break during refactoring.
  • One logical assertion per test: a test named "should return user when found" should not also test "should return null when not found." Separate concerns = separate tests.
  • Fast and isolated: no I/O, no network, no external services. Use test doubles for dependencies.
  • Readable: the test is documentation. it("returns empty array when no users match the filter") explains intent better than it("test filter edge case").

AAA pattern: Arrange (set up test data), Act (call the function), Assert (verify the result). Every unit test follows this structure explicitly.

Test Doubles

A test double replaces a real dependency in tests:

Dummy: passed but never used (a null or empty object to satisfy an interface parameter).

Stub: returns preset responses. userRepo.findById.returns({ id: 1, name: 'Alice' }). Stubs simulate dependencies without real behavior.

Mock: a stub that also verifies interactions. emailService.send.wasCalledOnce(). Mocks assert how the system under test interacted with its dependencies.

Fake: a real, working implementation that is simpler than production. An in-memory database (SQLite or just a Map) instead of PostgreSQL. Fast and realistic — better than mocks for most purposes.

Spy: a real implementation that also records calls. Used to verify behavior without stubbing.

The mock debate: over-mocking creates tests that pass even when the system is broken (wrong mock expectations), and tests that break when implementation details change without any real behavioral change. Martin Fowler distinguishes "mockists" (mock everything) from "classicists" (use real collaborators or fakes where possible). The classicist approach produces more durable tests.

Test-Driven Development (TDD)

TDD: write a failing test first, then write the minimal code to pass it, then refactor.

Benefits: forces you to think about the API before implementation (you write the calling code first); makes code testable by construction (hard-to-test code reveals design problems); gives instant feedback; prevents over-engineering (you only write code to pass tests).

The Red-Green-Refactor cycle:

  1. Red: write a test that fails (doesn't even compile yet is fine)
  2. Green: write the minimum code to make it pass (hack if needed)
  3. Refactor: clean up the code, knowing tests verify behavior is preserved

TDD is not universal. It works well for algorithms, business logic, and utility functions. It works poorly for UI, highly stateful systems, and exploratory prototypes. Skilled engineers apply it selectively.

Integration and E2E Testing

Integration tests should use real dependencies where practical. Testing an API with a real database (test database, populated with fixtures) catches more bugs than testing with mocks of the database layer. Use Docker Compose to spin up infrastructure for test suites.

Test fixtures: seed data that creates the starting state for each test. Each test should be isolated — no test should depend on the state left by another. Transaction rollback after each test keeps the database clean.

E2E tests with Playwright or Cypress automate browser interaction. They are valuable for critical user journeys (sign up, purchase, login) but should be minimized because they are slow and fail for reasons unrelated to bugs (timing issues, external service flakiness).

Property-Based Testing

Traditional example-based testing: check that reverse([1,2,3]) == [3,2,1].

Property-based testing: for any array A, reverse(reverse(A)) == A. Generate hundreds of random arrays; verify the property holds for all of them.

Properties to test: idempotence (applying twice = applying once), roundtrip (encode then decode = identity), invariants (sorted array stays sorted after insertion), commutativity (a+b = b+a).

fast-check (JavaScript), Hypothesis (Python), QuickCheck (Haskell) are the main frameworks. They shrink failing examples to the smallest case that still fails — invaluable for debugging.

Coverage and Quality

Code coverage measures how much code is executed during tests. 100% line coverage is achievable with trivial assertions that test nothing meaningful. Coverage is a floor, not a ceiling.

Branch coverage (testing both paths of every if-else) is more meaningful than line coverage. Mutation testing (deliberately introduce bugs and verify tests catch them) is the most rigorous test quality measure.

The goal is confidence, not coverage numbers. A well-tested critical path with 70% coverage is more valuable than 95% coverage with weak assertions.

0%