Hacker News

Modernizing a 25-year-old minimal C++ unit testing framework (Part 2)

Comments

C++ Code Capsules

In Part 1 of this two-part series I introduced a time-tested (i.e., old :-)) technique that handled automated unit testing in a remarkably simple way, including validating proper exception handling. The previous post left two problems on the table, however, and the journey to fix them turns out to be a nice tour of two key features of modern C++: inline variables and modules.

The simplicity of the test framework discussed in Part 1 follows from everything being contained in a small header file, test.h (include guards not shown):

namespace {
  std::size_t nPass = 0;
  std::size_t nFail = 0;

  inline void do_fail(const char* text,
                      const char* fileName,
                      long lineNumber) {
    std::cout
// C++20 module version.
import stack;
import std;
import test;

#include "test_macros.h"

int main() {
  stk;
  Stack test_(stk.size() == 0);

  // Test exceptions (top and pop are invalid on empty stack)
  throw_(stk.top(), std::underflow_error);
  throw_(stk.pop(), std::underflow_error);
  nothrow_(stk.size());

  // Test push and top
  .push(1);
  stktest_(stk.top() == 1);
  test_(stk.size() == 1);

  .push(2);
  stktest_(stk.top() == 2);
  test_(stk.size() == 2);

  // Test pop
  .pop();
  stktest_(stk.top() == 1);
  test_(stk.size() == 1);

  .pop();
  stktest_(stk.size() == 0);
  throw_(stk.top(), std::underflow_error);
  throw_(stk.pop(), std::underflow_error);

  report_();
}

The only difference in the client code is the use of import and including the header file after the import.

Key Differences Between the Two Approaches

If you're keeping score: both versions fix the ODR bug that started this series.

  • The inline variable fix costs you one keyword and works everywhere -std=c++17 does - no module-dependency scanner, no BMI worries, no compile-order gotchas to explain to a student at 11pm before an assignment is due.
  • If you have C++20 available, you could also replace __FILE__ and __LINE__ with std::source_location and still go header only.
  • The module version buys you real encapsulation (not just a polite namespace) and skips re-parsing the header in every translation unit, but it asks you to juggle two files and get the import-before-#include order right, on a toolchain story that's still finding its footing outside Visual C++.

Recommendation

My recommendation: default to the header-only version for coursework and anything that needs to build anywhere, on anything, today. Consider reaching for the module version once your build environment has genuinely solid modules support and you actually want the stronger boundary - treat it as the destination, not the daily driver, until the ecosystem catches up to the language. The module-#include hybrid approach is likely to be a reality for a long time to come.

(Note: This code was developed in Visual C++ on Visual Studio 2026 Community Edition. You can download the code here. I would like to thank Bjarne Stroustrup for his helpful comments on a previous draft of this post.)

Comments

No comments yet. Start the discussion.