C and C++ tests are executable programs whose status must affect the build and CI result. A framework can improve assertions and reporting, but a test executable with a dedicated main and deterministic dependencies is sufficient when integrated with the project’s test runner.
Build Tests as First-Class Targets #
Compile test targets separately from production binaries and link them against the production library. Register every executable with the existing test runner, such as CTest, Meson, or Automake TESTS, so a nonzero exit status fails CI.
add_executable(parser_test parser_test.cc)
target_link_libraries(parser_test PRIVATE parser)
add_test(NAME parser COMMAND parser_test)Keep test-only fixtures and fakes outside production interfaces where possible. A test runner should report the failing case, preserve diagnostics, and return nonzero when any required assertion or process fails.
Test Observable Behavior #
Test public behavior, error reporting, cleanup, and boundary conditions rather than private representation. Organize coverage by scope:
- Unit tests exercise a small component with controlled collaborators.
- Integration tests verify boundaries between real components, such as serialization, a database adapter, or a parser and scanner.
- End-to-end tests validate the assembled executable or service contract.
Include empty inputs, limits, invalid encodings, allocation or I/O failures where injectable, ownership transitions, and regressions for previously fixed defects. Tests should assert the promised result, not incidental implementation details.
Make Dependencies Deterministic #
Inject or wrap clocks, randomness, file systems, child processes, and network clients. Record a failing random seed and make it replayable. Use a temporary test workspace controlled by the test harness, unique ports or in-memory transports, and bounded timeouts. Avoid shared environment state and test ordering dependencies.
Run destructive or crash-prone tests in a subprocess. Process isolation also permits direct assertions about exit status, signals, standard output, standard error, and command-line behavior.
Use Runtime Diagnostics Alongside Tests #
Sanitizers find classes of defects only on executed paths, so they complement rather than replace functional tests.
c++ -fsanitize=address,undefined -fno-omit-frame-pointer -g \
parser_test.cc -o parser_test
./parser_testAddressSanitizer detects many invalid memory accesses; UndefinedBehaviorSanitizer diagnoses selected undefined operations. Enable the project-supported sanitizer configuration in CI, retain symbolized stacks and logs, and treat sanitizer reports as test failures.
Comments
Read or comment on GitHub (sign-in required to comment)