C++ PROGRAMMING ยท 12 MIN STUDY

Unit testing C++ programs with CTest

Separate logic from input/output, write repeatable tests, and register tests with CMake and CTest.

Learning objectives

  • Separate program logic from terminal input and output so it can be tested directly.
  • Write checks that return a failing process status when a test fails.
  • Register a test executable with CTest and run it from a build directory.
  • Distinguish CTest's test-running role from the assertions and fixtures supplied by a test framework.

Prerequisites: functions, separate compilation, and basic CMake usage. Review Functions, Header files, and CMake project builds first.

Why write tests?

A test checks a small behavior against an expected result. Tests make regressions visible: when code changes, the same checks can be run again. A useful unit test is focused, repeatable, and independent of manual keyboard input.

Separate logic from input/output

int add(int left, int right) { return left + right; }
// The calculation is easy to test without reading input or printing output.

A complete small test project

Keep the function declaration in a header, its definition in a source file, and the test executable in a separate source file. This makes the example match the multi-file CMake setup below.

Project layout

addition/
  CMakeLists.txt
  math_ops.hpp
  math_ops.cpp
  math_tests.cpp
math_ops.hpp
#pragma once

int add(int left, int right);
math_ops.cpp
#include "math_ops.hpp"

int add(int left, int right) {
    return left + right;
}
math_tests.cpp
#include "math_ops.hpp"
#include <iostream>

int main() {
    bool passed = true;
    auto check = [&passed](bool condition, const char* name) {
        if (!condition) {
            std::cerr << "FAILED: " << name << '\n';
            passed = false;
        }
    };

    check(add(2, 3) == 5, "positive values");
    check(add(-2, 2) == 0, "opposite values");
    check(add(0, 0) == 0, "zero values");
    return passed ? 0 : 1;
}

This test helper remains active in Debug and Release builds. The standard assert macro is useful for internal program checks, but defining NDEBUG disables it; a test executable must still report failures in the build configuration used by CI.

Register tests with CTest

cmake_minimum_required(VERSION 3.20)
project(Addition LANGUAGES CXX)
include(CTest)

add_library(math_ops math_ops.cpp)
target_compile_features(math_ops PUBLIC cxx_std_17)

add_executable(math_tests math_tests.cpp)
target_link_libraries(math_tests PRIVATE math_ops)

if(BUILD_TESTING)
    add_test(NAME math_tests COMMAND math_tests)
endif()

From the addition directory, configure and build with cmake -S . -B build and cmake --build build, then run ctest --test-dir build --output-on-failure. A successful run reports the registered test as passed. CTest runs registered test commands; it does not provide a full assertion framework by itself. Larger projects often use a dedicated framework such as GoogleTest or Catch2.

Design better test cases

  • Normal case: typical valid input.
  • Boundary case: minimum, maximum, empty, or one-element input.
  • Invalid case: malformed or out-of-range input.
  • Regression case: a test for a bug that was fixed.

Common mistakes

  • Testing only one happy-path input.
  • Making tests depend on execution order or external files without controlling them.
  • Using assert for checks that must run in production builds.
  • Testing implementation details rather than externally visible behavior.
  • Forgetting to register the executable with add_test.

Practice

  1. Write three tests for a function that returns the larger of two integers.
  2. Why should a unit test not depend on a user typing input?
  3. What is the difference between CTest and a test framework?
Show answers
  1. Try (2, 5), (-4, -1), and (7, 7).
  2. Automation must be repeatable and should not block waiting for a person.
  3. CTest discovers and runs registered test commands; a framework supplies assertions, fixtures, reporting, and other test-writing tools.

Summary

Keep core logic testable, cover normal and edge cases, and automate execution with CTest. Use a dedicated framework as test suites grow.

References