Appearance
Testing C and C++
Native tests prove behavior under one compiled configuration. Good suites also expose ownership, error paths, packaging, and platform assumptions that type checking alone cannot establish.
A C test can be a small executable
c
#include <assert.h>
#include <stddef.h>
static long total(const int *values, size_t count) {
long result = 0;
for (size_t i = 0; i < count; ++i) result += values[i];
return result;
}
int main(void) {
const int values[] = {3, 5, 8};
assert(total(values, 3) == 16);
assert(total(NULL, 0) == 0);
return 0;
}Production assert may disappear under NDEBUG, so a serious test harness should use checks that always execute and report failures. Lightweight custom macros work for small C libraries; established frameworks add registration, fixtures, filtering, and diagnostics.
C++ frameworks provide discovery and expressive checks
GoogleTest, Catch2, and doctest are common choices. Framework syntax matters less than test isolation, meaningful assertions, deterministic cleanup, and integration with the build. Acquire the framework through the project's declared dependency policy rather than requiring an undocumented global installation.
Test public behavior. Avoid reaching into private members solely to mirror implementation. A test that survives a sound refactor is more valuable than one coupled to call sequence details.
CTest orchestrates executables
CMake can register any test command:
cmake
include(CTest)
add_executable(report-tests tests/report_tests.cpp)
target_link_libraries(report-tests PRIVATE report)
add_test(NAME report.unit COMMAND report-tests)Run through ctest --test-dir build --output-on-failure. CTest handles selection, labels, timeouts, parallel execution, fixtures, and CI reporting; it is not itself an assertion framework.
Test failure and ownership behavior
Exercise empty inputs, maximum sizes, malformed bytes, allocation or I/O failures where injectable, and cleanup after partial construction. Sanitizers amplify the suite by detecting leaks, out-of-bounds accesses, use-after-free, and undefined operations along executed paths.
Death tests can verify intentional process termination but are platform-sensitive. Prefer ordinary error contracts when callers should recover.
Integration tests cross real boundaries
Unit tests suit pure parsing and algorithms. Integration tests can create temporary files, launch the CLI, inspect exit status/stdout/stderr, or connect components. Keep tests hermetic: use unique temporary directories, controlled clocks/random seeds, and bounded timeouts.
A packaging consumer test should install the library, configure a separate project with find_package, compile it, and run it. This validates a different product than an in-tree unit test.
Configuration matrices reveal assumptions
Run ownership and undefined-behavior tests in both debug and optimized builds, and run public API and packaging tests across the supported compilers, standards, static/shared variants, and operating systems. Not every combination must run on every change; use fast presubmit coverage and broader scheduled/release matrices.
Coverage shows which instrumented code executed, not whether assertions were meaningful or untested states are safe. Use it to find blind spots, not as the sole quality target.
Optional prompts
Explain: What does CTest add when a test framework already exists?
Answer: It orchestrates test executables and commands at the build/project level—selection, environment, parallelism, timeouts, and reporting—while the framework implements assertions inside a process.
Explain: Why run tests under optimization?
Answer: Optimization can expose undefined behavior and timing/lifetime assumptions hidden in debug builds; release flags are part of the shipped configuration.