Testing C and C++ Programs

Last update: 2022-10-24 10:04:22 +0800 +0800
Available translations: 日本語 / 中文(简体) / 中文(繁體)

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.

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

  1. Unit tests exercise a small component with controlled collaborators.
  2. Integration tests verify boundaries between real components, such as serialization, a database adapter, or a parser and scanner.
  3. 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.

bash
c++ -fsanitize=address,undefined -fno-omit-frame-pointer -g \
    parser_test.cc -o parser_test
./parser_test

AddressSanitizer 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)