Unit Testing Essentials
Plan isolated unit tests, select harnesses, and ensure code-level coverage — ISTQB Foundation Level Ch. 4
Learning Objectives
Concept Overview
Unit testing — also called component testing in ISTQB terminology — verifies the behaviour of a single, isolated software unit independently from the rest of the system. It sits at the base of the V-model and corresponds to the component design level.
Key principle from ISTQB FL syllabus (Section 4.1): "Component testing focuses on components that are separately testable. Objectives include reducing risk, verifying functional and non-functional behaviours of components, building confidence in the component's quality, and finding defects."
Position in the V-Model
| Development Phase | Test Level | Basis Document | Typical Defects Found |
|---|---|---|---|
| Component Design | Unit / Component Testing | Component spec, source code | Logic errors, missing paths, incorrect computation |
| System Design | Integration Testing | Architecture, interface spec | Interface mismatches, protocol errors |
| Requirements | System Testing | System requirements | Missing features, incorrect end-to-end flows |
| Business Requirements | Acceptance Testing | User stories, UAT scripts | Unmet business needs |
What Is a Unit?
A unit is the smallest piece of software that can be compiled, linked, loaded, and executed independently. Depending on the technology stack, a unit may be:
A single function or method within a class
A single class or a public method of a class
A single function or a compilation unit (.c file)
A single module export or function
Characteristics of a Good Unit for Testing
Test Isolation Techniques
Isolation means the unit under test (UUT) has no real dependency on external services, databases, file systems, or other modules. We replace dependencies with test doubles.
| Test Double | ISTQB Definition | Returns | Records Calls? | Typical Use |
|---|---|---|---|---|
| Stub | Provides canned answers to calls; does not verify interactions | Hardcoded / configured values | No | Replace a DB query with a fixed result set |
| Mock | Pre-programmed with expectations; verifies that calls were made correctly | Configurable | Yes (and asserts) | Verify an email service was called once with the right args |
| Fake | Working implementation, but simplified (in-memory) | Real logic, lightweight | No | In-memory repository instead of SQL DB |
| Spy | Wraps a real object and records calls; does not verify by default | Real values | Yes (passive) | Check how many times a logger was called |
| Driver | Code that calls the UUT (replaces a missing higher-level component) | N/A | No | A main() harness that invokes a library function under test |
Isolation Diagram
Anatomy of a Unit Test
Every unit test follows the Arrange–Act–Assert (AAA) pattern (also called Build–Operate–Check or Given–When–Then in BDD style).
Phase
Python (pytest) Example
Test Naming Conventions
| Convention | Pattern | Example |
|---|---|---|
| Classic | test_<method>_<scenario>_<expected> | test_calculate_tax_zero_income_returns_zero |
| BDD style | given_<context>_when_<action>_then_<outcome> | given_high_earner_when_tax_called_then_top_rate_applied |
| Should style | <method>_should_<outcome>_when_<scenario> | calculate_tax_should_return_zero_when_income_below_threshold |
FIRST Properties of Good Unit Tests
Runs in milliseconds; no I/O or network calls
No shared state between tests; order-independent
Same result every run, in any environment
Pass/fail without manual inspection of output
Written just before or alongside the code (TDD)
Test Harnesses & Frameworks
A test harness is the set of software and test data configured to test a unit by running it under various conditions and comparing actual vs. expected results.
A test framework provides: test runner, assertion library, test discovery, reporting, and (often) mocking utilities.
| Language | Primary Framework | Mocking Library | Coverage Tool | CI Integration |
|---|---|---|---|---|
| Python | pytest unittest | unittest.mock pytest-mock | coverage.py pytest-cov | GitHub Actions, GitLab CI |
| Java | JUnit 5 TestNG | Mockito EasyMock | JaCoCo | Maven Surefire, Jenkins |
| C# | NUnit xUnit MSTest | Moq NSubstitute | dotCover OpenCover | Azure DevOps, GitHub Actions |
| JavaScript | Jest Mocha+Chai | Jest mocks Sinon.js | Istanbul/nyc | GitHub Actions, CircleCI |
| C/C++ | Google Test CppUTest | Google Mock | gcov lcov | CMake + CTest |
Selecting a Harness — Decision Criteria
Prefer the de-facto standard (e.g., pytest for Python) to maximise community support and plugins.
Framework must have first-class support in the team's IDE for in-editor test execution and debugging.
Must produce machine-readable reports (JUnit XML, TAP, or LCOV) consumable by the CI pipeline.
Verify the coverage tool supports the target coverage criterion (statement, branch, MC/DC).
Built-in or companion mock library should support stubs, spies, and call verification without boilerplate.
For large suites, prefer frameworks that support parallel test execution (pytest-xdist, JUnit 5 parallel).
Code-Level Coverage Criteria
Coverage criteria define what proportion of the code structure has been exercised. ISTQB recognises coverage as the primary metric for assessing unit-test thoroughness.
| Criterion | Formula | Minimum Target | Detects | Limitation |
|---|---|---|---|---|
| Statement Coverage (SC) | Statements exercised / Total statements × 100% | 80–90% (industry typical) | Unreachable code, missing logic | Misses false branches; can reach 100% without testing all decisions |
| Branch Coverage (BC) | Branches exercised / Total branches × 100% | 70–80% (industry typical) | Untested decision outcomes (true/false) | Does not distinguish individual conditions in compound decisions |
| Condition Coverage (CC) | Conditions exercised T&F / Total conditions × 2 × 100% | Per project requirement | Conditions that never flip | Does not guarantee every condition independently influences the decision |
| MC/DC | Independence pairs per condition | Required: DO-178C Level A, ISO 26262 ASIL D | Masking faults between conditions | n+1 tests required; complex for large predicates |
| Path Coverage | Distinct paths exercised / All paths × 100% | Rarely 100% (exponential) | All feasible paths including loops | Infeasible for loops; exponential explosion |
Coverage Hierarchy
Industry Coverage Targets by Domain
| Domain | Standard | Required Criterion | Minimum % |
|---|---|---|---|
| Avionics Software | DO-178C Level A | MC/DC | 100% |
| Automotive (safety-critical) | ISO 26262 ASIL C/D | MC/DC | 100% |
| Medical Devices | IEC 62304 Class C | Branch Coverage | 100% |
| Enterprise Web Apps | Internal / SonarQube gate | Statement Coverage | 80% |
| Open Source Libraries | Codecov convention | Branch Coverage | 70–90% |
Test-Driven Development (TDD)
TDD is a development practice (not just a testing technique) where tests are written before the production code. It enforces testability by design and was popularised by Kent Beck (XP) and is referenced in ISTQB Advanced Level.
Red–Green–Refactor Cycle
Benefits of TDD
TDD Limitations
TDD Worked Mini-Example
Unit Test Planning
ISTQB requires that testing at every level follows a test plan. For unit testing this is typically a component-level test plan embedded in the development task or sprint story.
Unit Test Plan Template
| Section | Content | Example |
|---|---|---|
| Scope | Which modules/classes/functions are in scope | tax.py: functions calculate_tax(), get_bracket() |
| Test Objectives | What must the tests prove | Correct tax for all income brackets; zero for allowance; correct higher-rate threshold |
| Coverage Target | Criterion and minimum % | Branch coverage ≥ 80%; MC/DC for safety-critical path |
| Entry Criteria | Conditions that must hold before testing begins | Unit compiles without errors; static analysis passes; code review approved |
| Exit Criteria | Conditions to declare unit testing complete | All tests pass; coverage target met; no open Severity-1 defects |
| Test Environment | Language version, framework, CI pipeline | Python 3.12, pytest 7.4, pytest-cov, GitHub Actions |
| Test Data | Specific input values and expected outputs | income=0, 12570, 12571, 50270, 50271, 100000, -1 (invalid) |
| Schedule | When tests will be written and executed | TDD: tests written before implementation; CI gate on every PR |
| Roles | Who writes, who reviews, who signs off | Developer writes; peer reviews in PR; tech lead signs off coverage report |
| Risks | What could prevent meeting exit criteria | Complex legacy code without DI; time pressure causing test shortcuts |
Entry & Exit Criteria (ISTQB FL §5.2)
Entry Criteria
Exit Criteria
Worked Example: calculate_tax()
We derive a minimum test set that achieves 100% branch coverage for the UK income tax function.
Source Code
Control Flow Graph & Branch Analysis
Minimum Branch-Coverage Test Set
| Test Case | Input (income) | Expected Output | Branches Covered |
|---|---|---|---|
| TC-1 | -1 | raises ValueError | B1-T |
| TC-2 | 0 | 0.0 | B1-F, B2-T |
| TC-3 | 25,000 | 2,486.00 | B2-F, B3-T |
| TC-4 | 60,000 | 11,432.00 | B3-F, B4-T |
| TC-5 | 150,000 | 48,978.00 | B4-F |
5 test cases cover all 8 branches → 100% branch coverage achieved.
Pytest Implementation
Common Mistakes in Unit Testing
Class Activity (20 min)
Activity: Design a Unit Test Plan for validate_password()
Given the following function specification, work in pairs to:
- Identify all branches in the function.
- Derive a minimum test set achieving 100% branch coverage.
- Write at least 3 test cases in pytest AAA format (pseudo-code is acceptable).
- Identify which test doubles (if any) are required.
Function Specification
Submission Checklist
- List of branches (label B1-T, B1-F, B2-T, ... etc.)
- Test case table: TC-ID | Input | Expected | Branches covered
- 3 test functions in AAA format
- Coverage percentage achieved by your test set
Activity Rubric
| Criterion | Excellent (4) | Good (3) | Adequate (2) | Needs Work (1) |
|---|---|---|---|---|
| Branch Identification | All 8 branches correctly identified and labelled | 6–7 branches identified | 4–5 branches identified | Fewer than 4 branches |
| Test Coverage | 100% branch coverage with minimum test set | ≥ 75% branch coverage | ≥ 50% branch coverage | Below 50% |
| AAA Structure | Clear Arrange/Act/Assert in all 3 tests; meaningful names | AAA present; names acceptable | AAA partially present | No clear structure |
| Test Double Selection | Correctly identifies no test doubles needed (pure function) | Minor confusion on doubles | Proposed unnecessary mocks | No consideration of doubles |
Exit Ticket
Answer the following questions individually (5 min):
- A function has 3 independent boolean conditions combined with AND. How many test cases are needed for MC/DC coverage?
- What is the difference between a stub and a mock? Give one use case for each.
- You have 100% statement coverage but a colleague claims the tests are inadequate. What stronger criterion should they suggest and why?
- Name two exit criteria you would include in a unit test plan for a banking transaction function.
Model Answers
- n+1 = 4 tests — MC/DC requires n+1 tests where n is the number of conditions (3 conditions → 4 tests).
- Stub: returns hardcoded values, does not verify calls — use to replace a database query. Mock: verifies that specific calls were made with correct arguments — use to assert an email service was called exactly once.
- Branch coverage — statement coverage can miss the false branch of an if-statement; branch coverage requires both true and false outcomes of every decision to be tested.
- Any two of: all tests pass; branch coverage ≥ 80%; no open Severity-1 defects; coverage report archived in CI; boundary values for transaction amounts tested.
Summary & Assignment
Session Summary
| Concept | Key Takeaway |
|---|---|
| Unit Definition | Smallest independently testable component; scope varies by language (function, class, module) |
| Test Isolation | Replace real dependencies with stubs (canned data), mocks (verified calls), fakes (working lightweight impl), spies (passive recording) |
| AAA Pattern | Every test: Arrange → Act → Assert; one logical concept per test; FIRST properties |
| Harness Selection | Match framework to language ecosystem; ensure CI/CD compatibility and coverage reporting |
| Coverage Criteria | SC ⊂ BC ⊂ MC/DC; select criterion based on domain risk; 100% coverage ≠ 100% quality |
| TDD | Red–Green–Refactor; tests before code; produces executable specification; not a silver bullet |
| Unit Test Plan | Scope, objectives, coverage target, entry/exit criteria, environment, roles, risks |
Assignment — Lab Task
Implement and fully test the validate_password() function from the class activity:
- Write the production code in Python (or your team's agreed language).
- Use TDD: write failing tests first, then implement.
- Achieve 100% branch coverage using
pytest --cov. - Include boundary values and equivalence partition test cases.
- Submit: source file, test file, and a coverage HTML report screenshot.
Submission deadline: before the next lab session. Marks: 10 points — 4 for coverage, 3 for test quality (AAA, naming), 3 for correct implementation.
Further Reading
- ISTQB Foundation Level Syllabus v4.0 — Section 4.1: Component Testing
- Beck, K. (2002). Test-Driven Development: By Example. Addison-Wesley.
- Meszaros, G. (2007). xUnit Test Patterns: Refactoring Test Code. Addison-Wesley. (Test doubles taxonomy)
- Martin, R.C. (2008). Clean Code. Chapter 9: Unit Tests — FIRST properties.
- pytest documentation: docs.pytest.org