Overview

13 Testing and debugging

This chapter argues that correctness must be engineered deliberately, especially as software pervades safety‑critical contexts. It highlights how modern C++ features—strong typing, algebraic data types, and zero‑cost abstractions—can shift many classes of defects from runtime to compile time. By modeling domains precisely (for example, unit‑safe distance types and user‑defined literals) and favoring concise, functional styles, developers reduce surface area for bugs and catch mismatches early, preventing costly failures that would otherwise slip into execution.

When runtime checks are still needed, the chapter focuses on testing strategies that shine with pure functions. Because pure functions depend only on inputs and have no side effects, they are trivial to isolate, exercise across diverse representations, and reason about without setup of hidden state. The text shows how to automatically generate tests in two complementary ways: by synthesizing inputs from an inverse of the specification (e.g., strings with a target newline count) and by property‑based testing, where broad invariants (such as double‑reversal yielding the original, sort producing a nondecreasing sequence with preserved extrema) are validated over many random inputs. It also covers comparative testing, cross‑checking custom implementations against trusted references (e.g., persistent vectors versus std::vector, custom sort versus std::sort), and suggests recording RNG seeds for reproducible failures.

Finally, the chapter demonstrates testing monad‑oriented concurrent systems by exploiting the uniform interfaces of monads (bind/transform/join) to swap asynchronous streams for synchronous ranges during tests. By replacing I/O‑bound components with lightweight wrappers that capture expected replies, the same data‑flow pipeline can be validated deterministically without changing its core logic. This decoupling improves composability and makes interaction bugs easier to expose, while still encouraging dedicated tests for individual components like filters and transforms and for the real service layer when running end‑to‑end.

Summary

  • Every pure function is a good candidate to be unit tested. You know what it uses to calculate the result, and you know it doesn’t change any external state—its only effect is that it returns a result.
  • One of the most famous libraries that does property-checking against a randomly generated data set is Haskell’s QuickCheck. It inspired similar projects for many programming languages, including C++.
  • By changing the random function, you can change which types of tests are more often performed. For example, when generating random strings, you can favor shorter strings by using a random function with normal distribution and with mean zero.
  • Fuzzing is another testing method that uses random data. The idea is to test whether software works correctly given invalid (random) input. It’s highly useful for programs that accept unstructured input
  • Remembering the initial random seed allows you to replicate tests that have failed.
  • Correctly designed monadic systems should work without problems if the continuation monad or reactive streams are replaced with normal values and normal data collections. This allows you to switch on and off concurrency and asynchronous execution on the fly. You can use this switch during testing.

FAQ

How can I shift runtime errors to compile time in C++?By using strong types and higher-level abstractions. Examples include smart pointers for memory safety, algebraic data types like std::optional and std::variant, and unit-safe types with user-defined literals. For instance, modeling distances as distinct types (kilometers, miles) prevents mixing units at compile time instead of discovering issues at runtime.
Why are pure functions easier to unit test?Pure functions depend only on their inputs and have no side effects. This eliminates setup of external state, reduces the need to understand object internals, and lets you test the same function across many contexts (e.g., strings, ranges, streams) simply by supplying different inputs.
How can I automatically generate test cases for a pure function like count_lines?Use an inverse-style generator: for a desired line_count, generate random strings that contain exactly that many newline characters. Then assert that count_lines returns line_count. Limit the search space (e.g., maximum newline count, fixed number of samples), and vary inputs across runs.
What is property-based testing and how do I apply it?Property-based testing checks general properties that must hold for all inputs, not just specific examples. For a reverse function, properties include reverse(reverse(xs)) == xs, preserved length, and swapped first/last elements. Generate random inputs and verify all properties for each.
What is comparative (oracle) testing and when should I use it?Comparative testing verifies a new implementation against a trusted reference. Example: test an immutable BVT vector against std::vector by performing equivalent operations (push_back, pop_back, conversions) and asserting both structures contain the same elements after each step. Similarly, compare a custom sort to std::sort.
How do I make random tests reproducible?Record and print the random-number generator seed used for a test run. If a failure occurs, rerun with the same seed to reproduce and diagnose the issue. Also bound the test space with explicit limits (e.g., MAX_NEWLINE_COUNT, TEST_CASES_PER_LINE_COUNT).
Do compile-time guarantees remove the need for tests?No. While strong typing and FP abstractions move many errors to compile time and shorten code (reducing bug surface), logic errors and issues with real runtime data still occur. Unit, property-based, and comparative tests remain essential.
How should I structure unit tests for stateful classes versus pure functions?Stateful classes require arranging the object’s state, invoking methods, and asserting outcomes—often with knowledge of which state aspects matter. Pure functions skip setup entirely; you provide inputs and check outputs, focusing tests on behavior rather than state.
How can I test monad-based concurrent pipelines without dealing with timing and races?Exploit monadic uniformity. Replace asynchronous reactive streams with synchronous ranges in tests, and swap context-carrying types (e.g., with_socket → with_expected_reply). Lift transform/filter over the test wrapper and define a sink that asserts replies. The pipeline logic stays unchanged while tests run deterministically.
What practical steps tie these testing approaches together for algorithms like sort?- Generate inputs that invert the problem (shuffle a sorted vector; sorting must recover the original).
- Check essential properties (size preserved, min/max at ends, nondecreasing order, reverse invariance).
- Compare outputs against a known-good implementation (std::sort). Use random inputs with recorded seeds and bounded sizes.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Functional Programming in C++ ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Functional Programming in C++ ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Functional Programming in C++ ebook for free