Object-Oriented Testing Techniques
Design tests for classes, inheritance, polymorphism, and state behavior — Pressman Ch. 23 | Binder (1999)
Learning Objectives
- Derive test cases from a class state transition diagram using the all-states, all-transitions, and all-paths criteria.
- Apply the Round-Trip Scenario (RTS) testing strategy to generate comprehensive state-based test suites.
- Apply behavioural testing to a class using its specification rather than its implementation.
- Describe strategies for testing inheritance hierarchies: top-down and bottom-up retest approaches.
- Explain OO integration testing strategies: thread-based, use-case-based, and cluster testing.
- Identify which existing tests must be re-run when a class is modified (regression scope analysis in OO).
Techniques Overview
Session 6.5 established what makes OO testing different. This session establishes how to address those differences with concrete techniques. The primary techniques are organized by the OO challenge they address:
Addresses: hidden mutable state and state-dependent behavior. Derives tests from state transition diagrams.
Addresses: incomplete state coverage. Generates tests that exercise state sequences returning the object to its initial state.
Addresses: encapsulation (testing through the interface without implementation knowledge). Uses specification rather than code.
Addresses: the flattening problem. Determines which inherited tests must be rerun for each subclass.
Addresses: inter-object defects. Tests clusters of collaborating classes rather than individual classes.
Addresses: change impact in OO systems. Identifies which tests must be rerun after a class modification.
State-Based Testing
State-based testing derives test cases from the object's state machine model. A state machine specifies: states the object can be in, events (method calls) that trigger transitions, guards (conditions on transitions), and actions (outputs or side effects).
State Coverage Criteria
Three levels of rigor, in increasing order of test suite size:
- All-States: Every state must be reached at least once. Minimum viable criterion.
- All-Transitions: Every transition (edge) in the state diagram must be exercised at least once. Subsumes all-states. Standard criterion for most applications.
- All-Paths: Every distinct path from the initial state through the state machine must be exercised. Exponential growth; typically infeasible except for small machines.
| Current State | Event | Guard | Next State | Action |
|---|---|---|---|---|
| Open | deposit(amount) | amount > 0 | Open | balance += amount |
| Open | withdraw(amount) | amount ≤ balance | Open | balance -= amount |
| Open | withdraw(amount) | amount > balance | Open | throw InsufficientFunds |
| Open | close() | balance = 0 | Closed | — |
| Open | freeze() | — | Frozen | — |
| Frozen | unfreeze() | — | Open | — |
| Frozen | deposit(amount) | — | Frozen | throw AccountFrozenException |
| Closed | any operation | — | Closed | throw AccountClosedException |
State Test Worked Example
Applying all-transitions coverage to the BankAccount state machine yields the following required test cases:
| TC# | Initial State | Event | Expected Next State | What it tests |
|---|---|---|---|---|
| ST-1 | Open | deposit(100) | Open | Normal deposit |
| ST-2 | Open | withdraw(50) [bal=100] | Open | Sufficient funds withdrawal |
| ST-3 | Open | withdraw(150) [bal=100] | Open | Insufficient funds (exception) |
| ST-4 | Open (bal=0) | close() | Closed | Close on zero balance |
| ST-5 | Open | freeze() | Frozen | Freeze transition |
| ST-6 | Frozen | unfreeze() | Open | Unfreeze transition |
| ST-7 | Frozen | deposit(50) | Frozen | Operation blocked when frozen |
| ST-8 | Closed | deposit(50) | Closed | Operation blocked when closed |
Round-Trip Scenario Testing
Round-Trip Scenario (RTS) testing (Binder, 1999) generates test sequences that start in the initial state, traverse the state machine, and return to the initial state. This ensures all state transitions are exercised within realistic object lifecycles.
RTS Procedure
- Draw the state transition diagram for the class.
- Identify the initial state (post-construction).
- Generate all distinct round trips: paths from the initial state that visit each transition at least once and return to a stable state.
- For each round trip, write a test that drives the object through that state sequence.
- Include tests for illegal transitions (operations called in the wrong state).
- RTS-1 (Normal lifecycle): Construct → deposit → withdraw → deposit → withdraw (to zero) → close
- RTS-2 (Freeze/unfreeze cycle): Construct → deposit → freeze → [verify deposit rejected] → unfreeze → deposit → withdraw (to zero) → close
- RTS-3 (Illegal close): Construct → deposit → [attempt close with non-zero balance] → [verify exception] → withdraw (to zero) → close
Behavioural Testing of Classes
Behavioural testing treats the class as a black box and derives tests from its specification (contract, Javadoc, interface documentation). This technique respects encapsulation and produces tests that remain valid even if the implementation changes.
Specification-Based Test Derivation
For each public method, the specification defines: preconditions (what must hold before the call), postconditions (what must hold after a successful call), and exceptions (when they are thrown and what they mean).
Test cases are derived from: normal values satisfying preconditions, boundary values, precondition violations (expect exceptions), and postcondition verification sequences.
Stack.pop()
- Precondition: Stack must not be empty (size > 0)
- Postcondition: Returns the top element; size decremented by 1; element no longer in stack
- Exception: Throws
EmptyStackExceptionif stack is empty
Inheritance Hierarchy Testing Strategies
Two complementary strategies address testing across inheritance hierarchies:
Incremental Retest (Bottom-Up)
Test base classes first, then extend test suites as subclasses are added. When a subclass is created:
- Re-run all inherited test cases against the subclass
- Tests that pass: inherited behavior is preserved (no LSP violation)
- Tests that fail: an inherited method behaves differently in the subclass context — this is either intentional (and the test should be updated) or a defect
- Add new tests for subclass-specific methods and overridden behavior
Flattened Class Test Suite
Treat each concrete class as a complete, stand-alone unit. Derive a test suite that exercises the full observable interface of the class — including all inherited behavior — without reference to the inheritance hierarchy.
- More test cases overall (duplication across classes)
- Each class's test suite is self-contained and independent
- No need to trace which parent tests apply to which subclass
- Better isolation: a subclass test failure points directly to that class
OO Integration Testing
OO integration testing verifies that collaborating classes work correctly together. Because OO systems consist of many small, tightly coupled classes, integration defects at object boundaries are common.
Integration Strategies
Advantage: Tests complete, meaningful scenarios. Disadvantage: Complex setup; hard to isolate failure.
Advantage: Business-aligned. Disadvantage: High-level; may miss low-level collaboration defects.
Cluster Testing
A cluster is a small group of related classes that collaborate to provide a meaningful service (e.g., Order + OrderItem + Discount). Cluster testing integrates only the cluster before integrating with the rest of the system.
Process: (1) Define a cluster of 2–5 closely related classes. (2) Identify all collaboration points (method calls between classes). (3) Write integration tests that drive the cluster through its use cases. (4) Stub or mock classes outside the cluster. (5) Verify that cross-class interactions produce correct outputs and state.
Thread-Based and Use-Case Testing
A thread in the OO context is a path of execution that traverses multiple classes in response to a single stimulus (input event, user action, or system call). Thread-based testing creates test cases that follow these paths end to end.
Stimulus: User submits order with valid payment details.
Classes involved: OrderController → OrderService → InventoryService → PaymentService → NotificationService
Thread test verifies: order created, inventory decremented, payment charged, confirmation email queued. It is not practical to unit-test this interaction; it requires integration-level testing across all five classes.
Regression Testing in OO Systems
When a class is modified, which tests must be re-run? OO systems make this more complex than procedural code because of inheritance and polymorphic coupling.
All tests directly testing the modified class must be re-run.
If the modified class is a parent, all subclass test suites must be re-run (flattening problem).
Any class that depends on (calls) the modified class may be affected. Tests for these client classes must be re-run.
Any integration test or thread test that includes the modified class must be re-run.
Common Mistakes
Class Activity
State-Based Test Design (30 minutes)
Design a test suite for the TrafficLight class below using state-based testing and round-trip scenarios.
- Draw the state transition diagram (or describe it as a table).
- List all transitions. How many tests does all-transitions coverage require?
- Write three round-trip scenario test cases (describe the event sequence and assertions, you do not need to write code).
- Write two tests for illegal inputs or edge cases not covered by the normal transitions.
- Identify which tests would need to be re-run if an
EmergencyLightsubclass overridesadvance()to always transition to RED after GREEN.
Exit Ticket
- What is the difference between all-states and all-transitions coverage criteria? Which is stronger, and why?
- Describe the Round-Trip Scenario testing technique. What problem does it address compared to individual state transition tests?
- In behavioural testing, what sources of information are used to derive test cases? Why is this approach preferred over using the source code directly?
- Explain thread-based integration testing and give an example scenario where it would reveal a defect that unit tests would miss.
- When class X is modified, what four categories of tests must be re-run? Why can the regression scope be larger than just the tests for class X?
Summary & Preview
- State-based testing: derive tests from state transition diagrams; apply all-transitions coverage as the standard criterion.
- Round-Trip Scenario testing generates realistic state sequences that return to a stable state, ensuring lifecycle coverage.
- Behavioural testing: derive tests from the class specification (preconditions, postconditions, exceptions) without examining implementation.
- Inheritance hierarchy testing: re-run parent tests against subclasses; use flattened suites for regression.
- OO integration: thread-based, use-case-based, and cluster strategies target inter-object collaboration defects.
- Regression scope in OO includes the modified class, its subclasses, its client classes, and all integration tests involving those classes.
Session 6.7 addresses the unique challenges of testing web applications: client-server interactions, stateless HTTP, session management, browser compatibility, and the quality attributes most critical to web systems (usability, performance, security, reliability).