Testing for Backend Engineers: mocks, TDD and coverage
This video provides an in-depth exploration of software testing, focusing on practical testing strategies, understanding the core concepts of tests, and best practices for writing reliable, maintainable test suites. It highlights why tests are crucial, demystifies different test types, tackles common challenges such as flaky tests, and discusses measuring test and code quality. The content uses a Go-based task board application example, but the principles apply broadly across programming languages.
1. What Is a Test and Why Write One?
Testing is defined as writing a second program that exercises the main program to verify expected behavior and detect regressions. Tests feed inputs to the program and compare actual outputs against expected results.
- Primary purpose of tests: Catch regressions — bugs that reappear after they have been fixed.
- Tests serve multiple roles: documentation, guidance, or enforcement of requirements.
- Test-driven development (TDD) emphasizes writing tests before writing the code to shape the design and ensure clear goals.
Key insight: Writing poor or incomplete tests causes more harm than having no tests because they create false confidence.
2. Structure and Tools Surrounding Tests
- Tests are actual programs separate from the production application.
- A test runner automatically discovers and runs test files, then reports pass/fail outcomes.
- In Go, the test runner creates an auto-generated main test executable that runs all test functions.
- Assertions are checks inside tests that cause failures when conditions are unmet, usually implemented via libraries (except Go, which uses manual
ifchecks). - Meaningful failure messages in assertions are critical for debugging.
3. Types of Tests and Their Scope
Testing types differ mostly in how much of the system they involve and touch external resources.
- The Test Pyramid concept advocates many unit tests at the base, fewer integration, and even fewer E2E tests.
- An alternative mental model focuses on test scope in terms of environment interaction, defined as:
- Small tests: run inside one process, no network or DB
- Medium tests: access local resources (databases, caches) without external network
- Large tests: can touch anything, including external APIs
4. Mocking and Test Doubles
To isolate tests from external dependencies, test doubles stand in for real objects or services:
Best practice: Avoid 'mocking what you do not own.' Wrap external APIs behind interfaces, test your wrappers, and verify interactions only when necessary (e.g., outbound emails).
5. State vs. Interaction Verification
- State verification: Check the final state of the system after an operation (recommended).
- Interaction/behavior verification: Check how the system behaved step-by-step (sometimes necessary, but fragile).
- Avoid change detector tests that fail when implementation details change but the final outcome is correct, as they reduce refactorability.
6. Practical Challenges and Techniques
Database Testing
- In-memory fakes improve test speed but lose fidelity (realistic behavior) leading to missed bugs related to schema, constraints, indexing, or transactions.
- Recommended approach: Use a real database in tests with techniques to keep tests fast and isolated:
- Transaction per test: Begin a transaction at test start, rollback after test finishes.
- Template database: Use a pre-migrated template database; each test clones it.
- Tools like TestContainers allow spinning up real databases inside containers on demand, providing hermetic (sealed) testing environments.
Network and External APIs
- Wrap third-party APIs behind your own interfaces.
- For testing HTTP client code, start an actual HTTP server inside tests (e.g., with Go’s
httptest). - Libraries like MSW (for JS) intercept network calls to simulate responses.
Time and Asynchronous Testing
- Avoid brittle fixed-sleep pauses in tests that cause flakiness.
- Instead, wait for events or signals indicating completion.
- Another approach: inject controllable clock interfaces to simulate time passage deterministically.
Hermetic Testing
- Hermetic tests create their own isolated environment, avoiding dependence on external services or manual setups.
- They start and stop all resources they need (e.g., databases, clocks) within the test lifecycle.
7. Test-Driven Development (TDD)
TDD is a workflow with 3 steps:
- Red: Write a failing test (specification of behavior before implementation).
- Green: Write the minimal code to pass the test (may be imperfect).
- Refactor: Clean up code while keeping tests green.
Advantages:
- Helps design clean APIs (design by usage).
- Immediate feedback on requirements.
- Encourages writing tests that reflect real usage.
Limitations:
- Effective when expected output is clear (e.g., parsers, state machines).
- Not suited when exploring solutions or uncertain of design.
Philosophy debate: Some experts argue TDD can lead to over-mocking and brittle tests focused on implementation, rather than behavior. Testing state and observable outcomes instead of class internals is recommended.
8. Flaky Tests: Causes and Mitigation
- Flaky tests sometimes pass and sometimes fail without code changes, undermining confidence.
- Causes (from a 2014 study):
- Async/await timing issues: Waiting incorrectly on asynchronous processes.
- Concurrency issues: Race conditions within test code.
- Test order dependencies: Tests depend on artifacts or state created by previously run tests.
Best practices:
- Replace time-based waits with event-based signaling.
- Synchronize concurrent test accesses with proper primitives.
- Avoid implicit shared state; run tests in isolation.
- Handle flaky tests by:
- Deleting if redundant or unhelpful.
- Quarantining with metadata (author, date, reason).
- Avoid retrying automatically on failure.
9. Measuring Test Effectiveness
Code Coverage
- Measures percentage of code executed during tests.
- Types:
- Line coverage: Whether lines of code ran.
- Branch coverage: Whether all branches/conditions executed.
- Common guidance: aim for at least 80% line coverage.
- Limitations:
- Coverage shows execution, not necessarily that tests verify behavior.
- High coverage with poor assertions is misleading.
Mutation Testing
- Tests the tests by intentionally injecting small faults (mutants) into code.
- If tests fail on mutants (kill mutants) → tests are effective.
- If mutants survive → tests miss bugs or ineffective.
- Mutation score gives a better indicator of test quality than coverage alone.
10. Code Quality Metrics
Cyclomatic Complexity
- Counts independent paths through code (1 + number of decisions).
- High cyclomatic complexity implies harder-to-test and maintain code.
- Generally, complexity < 10 is considered reasonable.
Cognitive Complexity
- Measures how difficult the code is to understand, focusing on nesting depth rather than count of paths.
- Nested structures increase cognitive load more than flat switch statements.
- Helps prioritize readability improvements beyond mere cyclomatic counts.
Advice: Use complexity tools as guides or flags—not absolute rules—balancing maintainability with practical code design.
11. Static Analysis Tools to Improve Quality
- Linters: Detect suspicious code patterns, unused variables, unchecked errors.
- Formatters: Enforce consistent code style and readability.
- Type checkers: Catch type mismatches and potential errors at compile/edit time.
- Static analysis: Advanced pattern-based checks that trace data flow without running code.
These tools complement testing by catching certain classes of errors early.
This detailed presentation provides a holistic overview of modern testing methodologies, balancing theoretical principles with pragmatic advice focused on maintainability, reliability, and developer experience. It encourages focusing on meaningful assertions, hermetic testing environments, and practical trade-offs such as using real databases in CI pipelines. The video also contextualizes testing debates and evolving best practices, offering viewers a solid foundation for improving test suites and software quality.