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: 0

Step-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.