OOP Testing Strategies
Learning Objectives
Test state machines, polymorphic contracts and Liskov substitution.
Use property-based, fuzz and mutation testing.
Plan integration, end-to-end, concurrency, performance and regression tests.
Prerequisites: Modules 64–65.
Intuition and Definition
Illegal transitions accepted, legal ones rejected Polymorphic contract / LSP substitution tests Subtypes breaking the base contract
Violations of general laws over many generated inputs Crashes and hangs on malformed input Weak tests (mutants that survive) Integration / end-to-end Wiring and configuration errors
Concurrency tests (stress, ThreadSanitizer) Races and deadlocks Performance / regression Slowdowns and reappearing bugs
Why It Matters
Example-based unit tests check the cases you thought of. Properties, fuzzers and mutations find the ones you didn't.
Syntax and Language Notes
Property-based frameworks: Hypothesis (Python), jqwik (Java), FsCheck (.NET), fast-check (TypeScript), RapidCheck (C++).
Complete Example
Property-based testing by hand — generate many random inputs (seeded for reproducibility) and check laws that must always hold:
LSP substitution test — one test, run for every subtype:
import random
class Money:
def __init__(self, cents): self.cents = cents
def __add__(self, o): return Money(self.cents + o.cents)
def __eq__(self, o): return self.cents == o.cents
def split(self, parts): # split without losing cents
base, rest = divmod(self.cents, parts)
return [Money(base + (1 if i < rest else 0)) for i in range(parts)]
def buggy_split(m, parts): # the "obvious" version
return [Money(round(m.cents / parts)) for _ in range(parts)]
rng = random.Random(1234)
def check(prop, name, n=500):
for _ in range(n):
cents, parts = rng.randint(0, 10_000_00), rng.randint(1, 12)
if not prop(Money(cents), parts):
print(f"{name}: FAILS for cents={cents}, parts={parts}")
return
print(f"{name}: holds for {n} random cases")
check(lambda m, p: sum((x.cents for x in m.split(p))) == m.cents, "split preserves the total")
check(lambda m, p: max(x.cents for x in m.split(p)) - min(x.cents for x in m.split(p)) <= 1, "split is fair (differ by <= 1 cent)")
check(lambda m, p: (m + Money(5)) + Money(7) == m + (Money(5) + Money(7)), "addition is associative")
check
…split preserves the total: holds for 500 random cases
split is fair (differ by <= 1 cent): holds for 500 random cases
addition is associative: holds for 500 random cases
buggy split preserves the total: FAILS for cents=768385, parts=9#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Storage {
virtual ~Storage() = default;
virtual void put(const std::string& k, const std::string& v) = 0;
virtual std::string get(const std::string& k) const = 0; // contract: returns "" if absent, never throws
};
struct MapStorage : Storage {
std::vector<std::pair<std::string, std::string>> kv;
void put(const std::string& k, const std::string& v) override { kv.emplace_back(k, v); }
std::string get(const std::string& k) const override { for (auto it = kv.rbegin(); it != kv.rend(); ++it) if (it->first == k) return it->second; return ""; }
};
struct StrictStorage : MapStorage { // strengthens the precondition -> LSP violation
std::string get(const std::string& k) const override {
std::string v = MapStorage::get(k);
if (v.empty()) throw std::out_of_range("missing key " + k);
return v;
}
};
bool substitutionTest(Storage& s) { // written once against the base contract
try {
s.put("a", "1");
return s.get("a") == "1" && s.get("absent").empty();
} catch (const std::exception& e) {
st
…MapStorage substitutable: 1
threw: missing key absent
StrictStorage substitutable: 0Step-by-Step Walkthrough
Properties express laws ("splitting never loses money", "parts differ by at most one cent") instead of single examples.
500 random cases per property, seeded so failures are reproducible; the buggy rounding version fails quickly and the counterexample is printed.
The substitution test is written once against Storage's documented contract and run against each subtype; StrictStorage violates it by throwing for absent keys.
Common Mistakes
Random tests without a seed — failures can't be reproduced.
Properties that restate the implementation — choose independent laws (round trips, invariants, comparisons with a simple reference implementation).
Only end-to-end tests — slow, flaky and hard to diagnose.
No regression test for a fixed bug — it comes back.
Trade-offs and Alternatives
Property-based and fuzz tests find deep bugs but need good generators and can be slow; mutation testing measures test quality but is expensive to run (often nightly). Balance with the test pyramid: many unit tests, fewer integration tests, few end-to-end tests.
Real-World Use
Parsers and protocol stacks are fuzzed (libFuzzer, AFL, OSS-Fuzz); financial code uses property tests for rounding laws; safety projects use mutation testing to assess test suites; ThreadSanitizer runs in CI for concurrent code.
Practice
Q1. (Easy) What does mutation testing measure?
Answer: How well tests detect deliberately injected bugs (mutants); surviving mutants reveal weak tests.
Q2. (Medium) Give a property for a serializer.
Answer: Round trip: deserialize(serialize(x)) == x for all valid x.
Q3. (Advanced) How would you test a state machine thoroughly?
Answer: Enumerate every (state, event) pair: assert the resulting state for legal transitions and the rejection for illegal ones; add property tests that random event sequences never reach an invalid state; and test entry/exit actions per transition.
Interview Question
Q1. "How do you test that subclasses respect LSP?" Answer: Write the base type's contract as an abstract/parameterized test suite (preconditions accepted, postconditions guaranteed, invariants preserved, documented exceptions only) and run it against every implementation — including future ones, by making the suite reusable. Any subtype that fails isn't substitutable.
Quick Quiz
Q1. Why seed random test data?
Answer: To make failures reproducible.
Q2. Tool category for finding crashes on malformed input?
Answer: Fuzzers.
Q3. What is a regression test?
Answer: A test added for a fixed bug so it can't silently return.
Summary
Go beyond examples: state-transition, contract/LSP, property, fuzz and mutation tests.
Keep randomness reproducible; follow the test pyramid.
Related: Modules 19, 24, 64–65.