Module 4 Session 4.1 Component Testing

Unit Testing Essentials

Plan isolated unit tests, select harnesses, and ensure code-level coverage — ISTQB Foundation Level Ch. 4

Learning Objectives

LO 4.1.1 — Define a unit and explain what makes it the smallest independently testable component of software.
LO 4.1.2 — Distinguish isolation strategies: stubs, drivers, mocks, fakes, and spies.
LO 4.1.3 — Write a well-structured unit test following the Arrange–Act–Assert (AAA) pattern.
LO 4.1.4 — Select an appropriate test harness/framework for a given language and project context.
LO 4.1.5 — Apply statement, branch, and path coverage criteria to derive a minimum test set.
LO 4.1.6 — Outline the Red–Green–Refactor cycle of Test-Driven Development (TDD).
LO 4.1.7 — Produce a unit test plan specifying scope, entry/exit criteria, and coverage targets.

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 PhaseTest LevelBasis DocumentTypical Defects Found
Component DesignUnit / Component TestingComponent spec, source codeLogic errors, missing paths, incorrect computation
System DesignIntegration TestingArchitecture, interface specInterface mismatches, protocol errors
RequirementsSystem TestingSystem requirementsMissing features, incorrect end-to-end flows
Business RequirementsAcceptance TestingUser stories, UAT scriptsUnmet 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:

Python
A single function or method within a class
Java / C#
A single class or a public method of a class
C / C++
A single function or a compilation unit (.c file)
JavaScript
A single module export or function
Characteristics of a Good Unit for Testing
Single Responsibility — Does one thing, so a failing test points to exactly one cause.
Observable Output — Returns a value, mutates state, or raises an exception that can be asserted.
Controllable Inputs — All dependencies can be injected or replaced with test doubles.
Anti-pattern: God Method — A method that queries a DB, sends email, and computes tax is untestable as a unit without mocking everything.

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 DoubleISTQB DefinitionReturnsRecords 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
┌─────────────────────────────────────────────────────┐ │ TEST HARNESS │ │ │ │ [Driver / Test Method] │ │ │ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ Unit Under │◄── Stub: DB query result │ │ │ Test (UUT) │◄── Stub: Config values │ │ │ │──► Mock: Email service │ │ └─────────────────┘ (verifies call count/args) │ │ │ │ │ ▼ │ │ [Assertions on return value / state] │ └─────────────────────────────────────────────────────┘

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
Arrange — Set up preconditions: create the object, configure stubs/mocks, prepare input data.
Act — Invoke the unit under test with the prepared inputs. Exactly one call.
Assert — Verify that the actual output matches the expected output. May also verify side-effects (mock expectations).
Python (pytest) Example
# test_tax.py import pytest from tax import calculate_tax def test_standard_rate_applied(): # ── Arrange ─────────────────────── income = 50_000 expected_tax = 10_000 # 20% bracket # ── Act ─────────────────────────── actual_tax = calculate_tax(income) # ── Assert ──────────────────────── assert actual_tax == expected_tax
Test Naming Conventions
ConventionPatternExample
Classictest_<method>_<scenario>_<expected>test_calculate_tax_zero_income_returns_zero
BDD stylegiven_<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
F — Fast
Runs in milliseconds; no I/O or network calls
I — Isolated
No shared state between tests; order-independent
R — Repeatable
Same result every run, in any environment
S — Self-Validating
Pass/fail without manual inspection of output
T — Timely
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.

LanguagePrimary FrameworkMocking LibraryCoverage ToolCI 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
Language Ecosystem
Prefer the de-facto standard (e.g., pytest for Python) to maximise community support and plugins.
IDE Integration
Framework must have first-class support in the team's IDE for in-editor test execution and debugging.
CI/CD Compatibility
Must produce machine-readable reports (JUnit XML, TAP, or LCOV) consumable by the CI pipeline.
Coverage Reporting
Verify the coverage tool supports the target coverage criterion (statement, branch, MC/DC).
Mocking Capability
Built-in or companion mock library should support stubs, spies, and call verification without boilerplate.
Parallelism
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.

CriterionFormulaMinimum TargetDetectsLimitation
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
Stronger (subsumes) Weaker ◄─────────────────────────────────────────────────────────► Path Coverage ▲ subsumes MC/DC ▲ subsumes Branch/Decision Coverage (BC 100% ⟹ SC 100%) ▲ subsumes Statement Coverage Note: Branch Coverage SUBSUMES Statement Coverage MC/DC SUBSUMES Branch Coverage
Industry Coverage Targets by Domain
DomainStandardRequired CriterionMinimum %
Avionics SoftwareDO-178C Level AMC/DC100%
Automotive (safety-critical)ISO 26262 ASIL C/DMC/DC100%
Medical DevicesIEC 62304 Class CBranch Coverage100%
Enterprise Web AppsInternal / SonarQube gateStatement Coverage80%
Open Source LibrariesCodecov conventionBranch Coverage70–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
┌─────────────────────────────────────┐ │ RED │ │ Write a failing test for the │ │ next small piece of functionality │ └────────────────┬────────────────────┘ │ Test fails (expected) ▼ ┌─────────────────────────────────────┐ │ GREEN │ │ Write the MINIMUM production code │ │ to make the test pass │ └────────────────┬────────────────────┘ │ Test passes ▼ ┌─────────────────────────────────────┐ │ REFACTOR │ │ Clean up code (remove duplication, │ │ improve names) — tests stay green │ └────────────────┬────────────────────┘ │ Repeat for next requirement └──────────► RED
Benefits of TDD
Tests act as executable specification — always up to date.
100% coverage by construction — every line was written to pass a test.
Forces small, focused functions — untestable code doesn't get written.
Rapid defect detection — regression suite grows automatically.
TDD Limitations
Learning curve — developers often write tests after code (TAD: Test After Development).
Slow initial velocity — perceived overhead for simple CRUD.
Hard to apply to UI, hardware drivers, or legacy code without refactoring first.
Does not replace all other testing — integration and system tests still needed.
TDD Worked Mini-Example
# STEP 1 — RED: Write failing test def test_calculate_tax_returns_zero_for_zero_income(): assert calculate_tax(0) == 0 # NameError: calculate_tax not defined yet # STEP 2 — GREEN: Write minimum code def calculate_tax(income): return 0 # Simplest passing implementation # STEP 3 — Add next failing test def test_calculate_tax_standard_rate(): assert calculate_tax(50_000) == 10_000 # Fails — returns 0 # STEP 4 — GREEN: Expand implementation def calculate_tax(income): if income <= 12_570: # Personal allowance return 0 elif income <= 50_270: return (income - 12_570) * 0.20 else: return 7_540 + (income - 50_270) * 0.40 # STEP 5 — REFACTOR: Extract constants, improve readability PERSONAL_ALLOWANCE = 12_570 BASIC_RATE_LIMIT = 50_270 BASIC_RATE = 0.20 HIGHER_RATE = 0.40

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
SectionContentExample
ScopeWhich modules/classes/functions are in scopetax.py: functions calculate_tax(), get_bracket()
Test ObjectivesWhat must the tests proveCorrect tax for all income brackets; zero for allowance; correct higher-rate threshold
Coverage TargetCriterion and minimum %Branch coverage ≥ 80%; MC/DC for safety-critical path
Entry CriteriaConditions that must hold before testing beginsUnit compiles without errors; static analysis passes; code review approved
Exit CriteriaConditions to declare unit testing completeAll tests pass; coverage target met; no open Severity-1 defects
Test EnvironmentLanguage version, framework, CI pipelinePython 3.12, pytest 7.4, pytest-cov, GitHub Actions
Test DataSpecific input values and expected outputsincome=0, 12570, 12571, 50270, 50271, 100000, -1 (invalid)
ScheduleWhen tests will be written and executedTDD: tests written before implementation; CI gate on every PR
RolesWho writes, who reviews, who signs offDeveloper writes; peer reviews in PR; tech lead signs off coverage report
RisksWhat could prevent meeting exit criteriaComplex legacy code without DI; time pressure causing test shortcuts
Entry & Exit Criteria (ISTQB FL §5.2)
Entry Criteria
Source code compiles with no errors
Static analysis (linting) passes at agreed severity threshold
Unit dependencies are available (real or as test doubles)
Test framework and CI environment are configured and functional
Exit Criteria
All planned tests are executed
Coverage target is met (e.g., branch ≥ 80%)
No open Severity-1 or Severity-2 defects
Test results and coverage report are archived in CI

Worked Example: calculate_tax()

We derive a minimum test set that achieves 100% branch coverage for the UK income tax function.

Source Code
def calculate_tax(income: float) -> float: """Return income tax for UK 2024/25 rates.""" if income < 0: # Branch B1-T / B1-F raise ValueError("Income cannot be negative") if income <= 12_570: # Branch B2-T / B2-F return 0.0 elif income <= 50_270: # Branch B3-T / B3-F return (income - 12_570) * 0.20 elif income <= 125_140: # Branch B4-T / B4-F return 7_540.0 + (income - 50_270) * 0.40 else: return 37_698.0 + (income - 125_140) * 0.45
Control Flow Graph & Branch Analysis
START │ ├─[B1: income < 0]── True ──► raise ValueError ──► END │ │ False │ ├─[B2: income ≤ 12570]── True ──► return 0.0 ──► END │ │ False │ ├─[B3: income ≤ 50270]── True ──► return basic-rate tax ──► END │ │ False │ ├─[B4: income ≤ 125140]── True ──► return higher-rate tax ──► END │ │ False │ └──────────────────────────────► return additional-rate tax ──► END Total branches: 8 (B1-T, B1-F, B2-T, B2-F, B3-T, B3-F, B4-T, B4-F)
Minimum Branch-Coverage Test Set
Test CaseInput (income)Expected OutputBranches Covered
TC-1-1raises ValueErrorB1-T
TC-200.0B1-F, B2-T
TC-325,0002,486.00B2-F, B3-T
TC-460,00011,432.00B3-F, B4-T
TC-5150,00048,978.00B4-F

5 test cases cover all 8 branches → 100% branch coverage achieved.

Pytest Implementation
import pytest from tax import calculate_tax class TestCalculateTax: def test_negative_income_raises_value_error(self): with pytest.raises(ValueError): calculate_tax(-1) def test_zero_income_returns_zero(self): assert calculate_tax(0) == 0.0 def test_basic_rate_taxpayer(self): result = calculate_tax(25_000) assert result == pytest.approx(2_486.00, rel=1e-4) def test_higher_rate_taxpayer(self): result = calculate_tax(60_000) assert result == pytest.approx(11_432.00, rel=1e-4) def test_additional_rate_taxpayer(self): result = calculate_tax(150_000) assert result == pytest.approx(48_978.00, rel=1e-4) # Run with: pytest --cov=tax --cov-report=term-missing

Common Mistakes in Unit Testing

Testing Implementation, Not Behaviour — Tests that assert on internal variable names or private method calls break on every refactor. Test the public interface only.
Multiple Assertions Per Test (Over-asserting) — One test with 20 assertions gives one failure message but no indication of which assertion failed first. Keep one logical concept per test.
Shared Mutable State Between Tests — Tests that depend on execution order are fragile. Use setUp/tearDown or pytest fixtures with function scope.
Mocking Too Much — A test that mocks every dependency tests nothing real. Only mock at architectural boundaries (external services, I/O).
Ignoring Edge Cases — 100% statement coverage on the happy path still misses boundary values (0, -1, max int, empty string). Apply BVA alongside coverage criteria.
Confusing Coverage with Quality — 100% statement coverage guarantees every line was executed, not that every line was executed correctly. Assertions matter as much as coverage.
Skipping the Test Plan — Writing tests ad-hoc without defining entry/exit criteria leads to coverage gaps discovered at integration time.

Class Activity (20 min)

Activity: Design a Unit Test Plan for validate_password()

Given the following function specification, work in pairs to:

  1. Identify all branches in the function.
  2. Derive a minimum test set achieving 100% branch coverage.
  3. Write at least 3 test cases in pytest AAA format (pseudo-code is acceptable).
  4. Identify which test doubles (if any) are required.
Function Specification
def validate_password(password: str, username: str) -> bool: """ Returns True if the password meets all policy rules. Rules: 1. Length must be >= 8 characters 2. Must contain at least one digit 3. Must contain at least one uppercase letter 4. Must NOT contain the username (case-insensitive) Returns False otherwise. Does not raise exceptions. """
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
CriterionExcellent (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):

  1. A function has 3 independent boolean conditions combined with AND. How many test cases are needed for MC/DC coverage?
  2. What is the difference between a stub and a mock? Give one use case for each.
  3. You have 100% statement coverage but a colleague claims the tests are inadequate. What stronger criterion should they suggest and why?
  4. Name two exit criteria you would include in a unit test plan for a banking transaction function.
Model Answers
  1. n+1 = 4 tests — MC/DC requires n+1 tests where n is the number of conditions (3 conditions → 4 tests).
  2. 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.
  3. 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.
  4. 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
ConceptKey Takeaway
Unit DefinitionSmallest independently testable component; scope varies by language (function, class, module)
Test IsolationReplace real dependencies with stubs (canned data), mocks (verified calls), fakes (working lightweight impl), spies (passive recording)
AAA PatternEvery test: Arrange → Act → Assert; one logical concept per test; FIRST properties
Harness SelectionMatch framework to language ecosystem; ensure CI/CD compatibility and coverage reporting
Coverage CriteriaSC ⊂ BC ⊂ MC/DC; select criterion based on domain risk; 100% coverage ≠ 100% quality
TDDRed–Green–Refactor; tests before code; produces executable specification; not a silver bullet
Unit Test PlanScope, objectives, coverage target, entry/exit criteria, environment, roles, risks
Assignment — Lab Task

Implement and fully test the validate_password() function from the class activity:

  1. Write the production code in Python (or your team's agreed language).
  2. Use TDD: write failing tests first, then implement.
  3. Achieve 100% branch coverage using pytest --cov.
  4. Include boundary values and equivalence partition test cases.
  5. 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