Module 6 Session 6.6 OO Testing Techniques

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:

State-Based Testing
Addresses: hidden mutable state and state-dependent behavior. Derives tests from state transition diagrams.
Round-Trip Scenario Testing
Addresses: incomplete state coverage. Generates tests that exercise state sequences returning the object to its initial state.
Behavioural Testing
Addresses: encapsulation (testing through the interface without implementation knowledge). Uses specification rather than code.
Inheritance Hierarchy Testing
Addresses: the flattening problem. Determines which inherited tests must be rerun for each subclass.
OO Integration Testing
Addresses: inter-object defects. Tests clusters of collaborating classes rather than individual classes.
Regression Scope Analysis
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.
State Transition Table: BankAccount
Current StateEventGuardNext StateAction
Opendeposit(amount)amount > 0Openbalance += amount
Openwithdraw(amount)amount ≤ balanceOpenbalance -= amount
Openwithdraw(amount)amount > balanceOpenthrow InsufficientFunds
Openclose()balance = 0Closed
Openfreeze()Frozen
Frozenunfreeze()Open
Frozendeposit(amount)Frozenthrow AccountFrozenException
Closedany operationClosedthrow AccountClosedException

State Test Worked Example

Applying all-transitions coverage to the BankAccount state machine yields the following required test cases:

TC#Initial StateEventExpected Next StateWhat it tests
ST-1Opendeposit(100)OpenNormal deposit
ST-2Openwithdraw(50) [bal=100]OpenSufficient funds withdrawal
ST-3Openwithdraw(150) [bal=100]OpenInsufficient funds (exception)
ST-4Open (bal=0)close()ClosedClose on zero balance
ST-5Openfreeze()FrozenFreeze transition
ST-6Frozenunfreeze()OpenUnfreeze transition
ST-7Frozendeposit(50)FrozenOperation blocked when frozen
ST-8Closeddeposit(50)ClosedOperation blocked when closed
// ST-5 and ST-6 implemented as JUnit 5 tests @Test void freeze_transitions_account_to_frozen_state() { BankAccount acc = new BankAccount(200.0); acc.freeze(); assertThat(acc.getStatus()).isEqualTo("FROZEN"); } @Test void deposit_on_frozen_account_throws_AccountFrozenException() { BankAccount acc = new BankAccount(200.0); acc.freeze(); assertThrows(AccountFrozenException.class, () -> acc.deposit(50.0)); }

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
  1. Draw the state transition diagram for the class.
  2. Identify the initial state (post-construction).
  3. Generate all distinct round trips: paths from the initial state that visit each transition at least once and return to a stable state.
  4. For each round trip, write a test that drives the object through that state sequence.
  5. Include tests for illegal transitions (operations called in the wrong state).
RTS Scenarios for BankAccount:
  • 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.

Example: Specification for 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 EmptyStackException if stack is empty
@Test void pop_returns_last_pushed_element() { // postcondition: returns top Stack<Integer> s = new Stack<>(); s.push(42); assertThat(s.pop()).isEqualTo(42); } @Test void pop_decrements_size() { // postcondition: size -1 Stack<Integer> s = new Stack<>(); s.push(1); s.push(2); s.pop(); assertThat(s.size()).isEqualTo(1); } @Test void pop_on_empty_stack_throws() { // precondition violation Stack<Integer> s = new Stack<>(); assertThrows(EmptyStackException.class, s::pop); }

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:

  1. Re-run all inherited test cases against the subclass
  2. Tests that pass: inherited behavior is preserved (no LSP violation)
  3. 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
  4. 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
Practical recommendation: Use the incremental retest approach during active development (it is faster). Use the flattened approach for the official regression suite (it is more reliable and self-contained).

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
Thread-Based Integration: Integrates all classes needed to respond to a single input event or use case scenario. Tests the vertical slice of the system for one specific user interaction.
Advantage: Tests complete, meaningful scenarios. Disadvantage: Complex setup; hard to isolate failure.
Use-Case-Based Integration: Each use case (login, checkout, report generation) drives the integration of all participating classes. Integration tests map directly to business functionality.
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.

OO integration does not replace unit testing: Integration tests detect inter-object defects that unit tests cannot find. They are slower and harder to diagnose. The correct approach is thorough unit testing first, then cluster/integration testing, not substituting one for the other.

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.

Example: "Place Order" Thread

Stimulus: User submits order with valid payment details.

Classes involved: OrderControllerOrderServiceInventoryServicePaymentServiceNotificationService

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.

Modified class tests
All tests directly testing the modified class must be re-run.
Subclass tests
If the modified class is a parent, all subclass test suites must be re-run (flattening problem).
Client class tests
Any class that depends on (calls) the modified class may be affected. Tests for these client classes must be re-run.
Integration and thread tests
Any integration test or thread test that includes the modified class must be re-run.
Change impact analysis: Build dependency graphs or use static analysis tools to identify which classes depend on a changed class. This determines the minimum regression test scope. Running the full test suite on every change is ideal but not always practical for large systems.

Common Mistakes

Skipping state-based tests for stateful objects: Testing only methods in isolation for a stateful object like a connection pool, session, or account misses most real defects, which appear when methods are called in sequences that produce unexpected state interactions.
Not rerunning inherited tests on subclasses: This is the most common OO regression mistake. An override that inadvertently breaks an inherited contract may go undetected for months.
Integration-testing instead of unit-testing: Teams that find unit testing OO classes difficult sometimes resort to testing classes only through integration tests. This makes test failures hard to diagnose and misses many unit-level defects.
Omitting illegal transition tests: Testing only valid state transitions misses robustness defects. Always include tests for operations called in illegal states (e.g., withdrawing from a closed account).

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.

class TrafficLight { // States: RED, GREEN, YELLOW // RED -[advance()]-> GREEN // GREEN -[advance()]-> YELLOW // YELLOW -[advance()]-> RED // All states: emergencyStop() -> RED // RED: canCross() returns false // GREEN: canCross() returns true // YELLOW: canCross() returns false }
  1. Draw the state transition diagram (or describe it as a table).
  2. List all transitions. How many tests does all-transitions coverage require?
  3. Write three round-trip scenario test cases (describe the event sequence and assertions, you do not need to write code).
  4. Write two tests for illegal inputs or edge cases not covered by the normal transitions.
  5. Identify which tests would need to be re-run if an EmergencyLight subclass overrides advance() to always transition to RED after GREEN.

Exit Ticket

  1. What is the difference between all-states and all-transitions coverage criteria? Which is stronger, and why?
  2. Describe the Round-Trip Scenario testing technique. What problem does it address compared to individual state transition tests?
  3. In behavioural testing, what sources of information are used to derive test cases? Why is this approach preferred over using the source code directly?
  4. Explain thread-based integration testing and give an example scenario where it would reveal a defect that unit tests would miss.
  5. 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

Key takeaways from Session 6.6:
  • 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.
Coming up — Session 6.7: Testing Web-Based Systems
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).