Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 15 – Build systems, testing and tooling

Unit tests with GoogleTest or Catch2 and CTest

Write C++ unit tests with GoogleTest 1.18 or Catch2 3.16, register them with CTest through CMake, read a failing run, and run the suite under sanitizers.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: A framework writes the checks, CTest runs the tests · Six GoogleTest tests catch the operator+ bug · Check yourself. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Write unit tests for a class with a test framework
  • Add the tests to CTest and run them with ctest
  • Read a failing test's report and find the cause
  • Run the same suite under AddressSanitizer and UBSan with a test preset

Before you start

In the Fraction class of this lesson, an operator+ that adds the denominators instead of multiplying them compiles, runs and returns 1/1 for 1/6 + 1/3. A unit test is a small program that calls one piece of your code, a function or a class, and checks what it returns, so that mistake fails a test. In C++ the job is split between two tools. A test framework provides the macros you write tests with and the report when a check fails; GoogleTest and Catch2 are two open-source frameworks for this, and doctest is a lighter third. CTest, which ships with CMake, is the runner: it keeps the list of every test in the project, runs them, in parallel if you ask, and says which ones failed, with an exit status: The number a program hands back when it ends, the value main returns. 0 means success; a shell keeps the last status in $?, and && runs the next command only after a status of 0. More in “How C++ compiles: preprocess, compile and link” that a CI job can act on.

This lesson writes unit tests with GoogleTest and CTest for that Fraction class, reads the failing run, and shows what the same tests look like in Catch2.

A framework writes the checks, CTest runs the tests

CMake joins the two tools: FetchContent downloads a pinned framework version at configure time, enable_testing() turns testing on, and gtest_discover_tests (or the Catch2 and doctest equivalents) registers each test with CTest. The three frameworks compare like this:

GoogleTest 1.18.0 Catch2 3.16.1 doctest 2.5.3
Licence BSD-3-Clause Boost Software License 1.0 MIT
Oldest C++ it supports C++17 C++14 C++11
A test looks like TEST(Fraction, Multiplies) with EXPECT_EQ(a, b) TEST_CASE("Fraction multiplies") with CHECK(a == b) TEST_CASE("Fraction multiplies") with CHECK(a == b)
Built as Two static libraries, gtest and gtest_main Static libraries Catch2 and Catch2WithMain A single header, plus an optional library with main
Known for gMock for mock objects, death tests, parameterised tests SECTIONs, generators, matchers, benchmarks Fast compiles; tests can live next to the code
Registered with CTest by gtest_discover_tests, in CMake’s GoogleTest module catch_discover_tests, in Catch2’s extras/Catch.cmake doctest_discover_tests, in doctest’s scripts/cmake/doctest.cmake

GoogleTest’s checks name what they compare: EXPECT_EQ keeps going after a failure, ASSERT_EQ stops the test, and EXPECT_THROW checks that an exception is thrown. Catch2 and doctest look at the whole expression instead, so a plain CHECK(a == b) reports both values when it fails. CTest knows nothing about any framework: a test is a command, and it passes when that command exits with status 0. test discovery: Asking a built test program for its list of tests, as gtest_discover_tests does after each build, so that each test becomes a CTest test of its own that can be run, selected and timed alone. asks the built test program for its list of tests, so each TEST or TEST_CASE becomes its own CTest test that can be run, selected and timed alone.

The diagram follows one test file from its source to CTest’s report:

A test file becomes a test program, the program lists its tests for CTest, and ctest runs each one and reports the result.fraction_gtest.cppTEST(...) blocks, written with GoogleTestfraction_gtestone test program, with main from gtest_mainCTest's test listone entry per TESTctest reportpassed and failed tests, exit status 0 or 8cmake --buildgtest_discover_tests asks for --gtest_list_testsctest runs each entry as its own process

From a test file to a CTest report

Text description of the diagram

The diagram is a column of four boxes joined by arrows.

  1. fraction_gtest.cpp holds the TEST blocks, written with GoogleTest.
  2. cmake --build compiles it into one test program, fraction_gtest, whose main function comes from the gtest_main library.
  3. After the build, gtest_discover_tests runs the program with --gtest_list_tests and puts one entry per TEST into CTest's test list.
  4. ctest runs each entry as its own process and reports the passed and failed tests, ending with exit status 0 when all passed and 8 when any failed.

Checkpoint

Check: A framework writes the checks, CTest runs the tests

  1. Question 1 of 2 How does CTest decide that a test passed?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The test's command exited with status 0

    A CTest test is just a command. Frameworks such as GoogleTest make the test program exit with a non-zero status when a check fails, and that is all CTest looks at; a test that runs too long fails too.

  2. Question 2 of 2 What does gtest_discover_tests(fraction_gtest) do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: After each build it asks the test program for its tests and adds one CTest test per TEST

    Discovery runs the built program with --gtest_list_tests, so a new TEST appears in ctest without running CMake again. That is why the lesson's ctest output listed six tests by name.

More that the runner and the frameworks can do

  • Catch2’s SECTIONs run one test case once per section, each time from a fresh copy of its setup code.
  • doctest is a single header that compiles quickly, and its tests may sit in the same file as the code.
  • Test presets in CMakePresets.json give a CTest run a name, so developers and CI run exactly the same suite.
  • sanitizer: A compiler option, such as -fsanitize=address or -fsanitize=undefined, that builds extra checks into a program, so that memory errors and undefined behaviour are reported, with their source line, while it runs. More in “Finding memory bugs with AddressSanitizer and UBSan” builds run the same tests with AddressSanitizer and UBSan, which turn hidden memory errors and undefined behaviour into reports.
  • ctest -j 8 runs tests in parallel, -R selects them by name, and --repeat until-fail:50 repeats them to expose a flaky test.
  • Fixtures and parameterised tests (TEST_F, TEST_P) share setup between tests and run one test over many inputs.

Six GoogleTest tests catch the operator+ bug

Fraction keeps a fraction in lowest terms with a positive denominator and throws std::invalid_argument for a zero denominator. The tests check one behaviour each, and their names say which:

The class and its GoogleTest tests

C++ header · fraction/include/fraction/fraction.hpp

#pragma once

#include <cstddef>
#include <ostream>
#include <string>

namespace frac {

// A fraction kept in lowest terms, with a positive denominator: Fraction(2, -4) is -1/2.
class Fraction {
public:
    Fraction(long long numerator, long long denominator);  // throws std::invalid_argument for a zero denominator

    long long numerator() const { return num_; }
    long long denominator() const { return den_; }
    std::string to_string() const;

    friend Fraction operator+(const Fraction& a, const Fraction& b);
    friend Fraction operator*(const Fraction& a, const Fraction& b);
    friend bool operator==(const Fraction& a, const Fraction& b) = default;

private:
    long long num_;
    long long den_;
};

std::ostream& operator<<(std::ostream& out, const Fraction& f);

// A hash, so that fractions can go into an unordered_set.
std::size_t hash_value(const Fraction& f);

}  // namespace frac

C++ (fraction.cpp) · fraction/src/fraction.cpp

#include "fraction/fraction.hpp"

#include <numeric>
#include <stdexcept>

namespace frac {

Fraction::Fraction(long long numerator, long long denominator) {
    if (denominator == 0) throw std::invalid_argument("denominator is zero");
    if (denominator < 0) {
        numerator = -numerator;
        denominator = -denominator;
    }
    const long long g = std::gcd(numerator, denominator);
    num_ = numerator / g;
    den_ = denominator / g;
}

std::string Fraction::to_string() const { return std::to_string(num_) + "/" + std::to_string(den_); }

Fraction operator+(const Fraction& a, const Fraction& b) {
#ifdef FRACTION_INJECT_BUG
    // The bug a test should catch: the denominators are added instead of multiplied.
    return Fraction(a.num_ * b.den_ + b.num_ * a.den_, a.den_ + b.den_);
#else
    return Fraction(a.num_ * b.den_ + b.num_ * a.den_, a.den_ * b.den_);
#endif
}

Fraction operator*(const Fraction& a, const Fraction& b) { return Fraction(a.num_ * b.num_, a.den_ * b.den_); }

std::ostream& operator<<(std::ostream& out, const Fraction& f) { return out << f.to_string(); }

std::size_t hash_value(const Fraction& f) {
    // Signed arithmetic: overflows, which is undefined behaviour, once the numerator passes about 9.2e12.
    const long long h = f.numerator() * 1'000'003 + f.denominator();
    return static_cast<std::size_t>(h);
}

}  // namespace frac

C++ (fraction_gtest.cpp) · fraction/tests/fraction_gtest.cpp

#include <stdexcept>

#include <gtest/gtest.h>

#include "fraction/fraction.hpp"

using frac::Fraction;

TEST(Fraction, StoresLowestTerms) {
    const Fraction f(6, 8);
    EXPECT_EQ(f.numerator(), 3);
    EXPECT_EQ(f.denominator(), 4);
}

TEST(Fraction, KeepsTheSignInTheNumerator) {
    EXPECT_EQ(Fraction(2, -4), Fraction(-1, 2));
}

TEST(Fraction, AddsWithDifferentDenominators) {
    EXPECT_EQ(Fraction(1, 6) + Fraction(1, 3), Fraction(1, 2));
}

TEST(Fraction, Multiplies) {
    EXPECT_EQ(Fraction(2, 3) * Fraction(9, 4), Fraction(3, 2));
}

TEST(Fraction, RejectsAZeroDenominator) {
    EXPECT_THROW(Fraction(1, 0), std::invalid_argument);
}

TEST(Fraction, EqualFractionsHashEqual) {
    EXPECT_EQ(frac::hash_value(Fraction(1, 2)), frac::hash_value(Fraction(2, 4)));
    EXPECT_EQ(frac::hash_value(Fraction(9'500'000'000'000, 7)), frac::hash_value(Fraction(19'000'000'000'000, 14)));
}
fraction/CMakeLists.txt Text · fraction/CMakeLists.txt
cmake_minimum_required(VERSION 3.28...4.4)
project(fraction LANGUAGES CXX)

set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)

add_library(fraction src/fraction.cpp)
target_include_directories(fraction PUBLIC include)
target_compile_features(fraction PUBLIC cxx_std_23)
option(FRACTION_INJECT_BUG "Build operator+ with a bug, to see a failing test" OFF)
if(FRACTION_INJECT_BUG)
  target_compile_definitions(fraction PRIVATE FRACTION_INJECT_BUG)
endif()

# Tests: only when this is the top-level project, so projects that add fraction as a dependency do not build them.
if(PROJECT_IS_TOP_LEVEL)
  enable_testing()
  include(FetchContent)

  # GoogleTest 1.18.0, pinned by its archive's SHA-256: a changed download fails the configure step.
  FetchContent_Declare(googletest
    URL https://github.com/google/googletest/releases/download/v1.18.0/googletest-1.18.0.tar.gz
    URL_HASH SHA256=6e3191c1455468b3fc35a417fb565c1c5071aee1b7e7f85e30cf48a98d37d8b5)
  set(BUILD_GMOCK OFF CACHE BOOL "" FORCE)       # this suite uses no mocks
  set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)     # nothing of GoogleTest is installed with the project
  set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)  # Windows: the same C run-time library as the project
  FetchContent_MakeAvailable(googletest)

  add_executable(fraction_gtest tests/fraction_gtest.cpp)
  target_link_libraries(fraction_gtest PRIVATE fraction GTest::gtest_main)
  include(GoogleTest)
  gtest_discover_tests(fraction_gtest)

  # The same tests with Catch2 3.16.1 and doctest 2.5.3, when asked for.
  option(FRACTION_CATCH2 "Also build the Catch2 version of the tests" OFF)
  if(FRACTION_CATCH2)
    FetchContent_Declare(Catch2
      URL https://github.com/catchorg/Catch2/archive/refs/tags/v3.16.1.tar.gz
      URL_HASH SHA256=d16bd1b8b2364918bd472cb216394acb93bde6784890df0d91bccfb3f28d777d)
    FetchContent_MakeAvailable(Catch2)
    add_executable(fraction_catch2 tests/fraction_catch2.cpp)
    target_link_libraries(fraction_catch2 PRIVATE fraction Catch2::Catch2WithMain)
    list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)
    include(Catch)
    catch_discover_tests(fraction_catch2 TEST_PREFIX "catch2: ")
  endif()

  option(FRACTION_DOCTEST "Also build the doctest version of the tests" OFF)
  if(FRACTION_DOCTEST)
    FetchContent_Declare(doctest
      URL https://github.com/doctest/doctest/archive/refs/tags/v2.5.3.tar.gz
      URL_HASH SHA256=174ebc4e769928959614789c5b4e9c3d0a0f81a62bb608756b127bfebfb21331)
    FetchContent_MakeAvailable(doctest)
    add_executable(fraction_doctest tests/fraction_doctest.cpp)
    target_link_libraries(fraction_doctest PRIVATE fraction doctest::doctest_with_main)
    include(${doctest_SOURCE_DIR}/scripts/cmake/doctest.cmake)
    doctest_discover_tests(fraction_doctest TEST_PREFIX "doctest: ")
  endif()
endif()

FetchContent_Declare names the GoogleTest 1.18.0 release archive and its SHA-256, so the configure step fails if the download ever differs. PROJECT_IS_TOP_LEVEL builds the tests only when this is the project being built, not when another project adds fraction as a dependency. gtest_main supplies main, and gtest_discover_tests runs the test program after each build to list its tests for CTest.

Six passing tests, then one failing Python · gtest_session.py
from session import run

# Configure (CMake downloads GoogleTest 1.18.0 from GitHub here: this session needs the network), build, run the tests.
run("""
cd fraction
cmake -S . -B build -G Ninja
""", keep=r"^(?!-- (?:The|Detecting|Check|Configuring|Generating|Performing|Found))")
run("""
cmake --build build
ctest --test-dir build --output-on-failure
""")
# The same tests against a library whose operator+ has a bug.
run("""
cmake -S . -B build -DFRACTION_INJECT_BUG=ON
cmake --build build
ctest --test-dir build --output-on-failure
""", keep=r"^(?!-- (?:Configuring|Generating|Build files))", max_lines=60)

Output

$ cd fraction
$ cmake -S . -B build -G Ninja
-- Build files have been written to: build
[17 lines not shown]
$ cmake --build build
[1/8] Building CXX object _deps/googletest-build/googletest/CMakeFiles/gtest.dir/src/gtest-all.cc.o
[2/8] Building CXX object CMakeFiles/fraction.dir/src/fraction.cpp.o
[3/8] Linking CXX static library lib/libgtest.a
[4/8] Building CXX object _deps/googletest-build/googletest/CMakeFiles/gtest_main.dir/src/gtest_main.cc.o
[5/8] Linking CXX static library libfraction.a
[6/8] Building CXX object CMakeFiles/fraction_gtest.dir/tests/fraction_gtest.cpp.o
[7/8] Linking CXX static library lib/libgtest_main.a
[8/8] Linking CXX executable fraction_gtest
$ ctest --test-dir build --output-on-failure
Test project build
    Start 1: Fraction.StoresLowestTerms
1/6 Test #1: Fraction.StoresLowestTerms ...............   Passed    … sec
    Start 2: Fraction.KeepsTheSignInTheNumerator
2/6 Test #2: Fraction.KeepsTheSignInTheNumerator ......   Passed    … sec
    Start 3: Fraction.AddsWithDifferentDenominators
3/6 Test #3: Fraction.AddsWithDifferentDenominators ...   Passed    … sec
    Start 4: Fraction.Multiplies
4/6 Test #4: Fraction.Multiplies ......................   Passed    … sec
    Start 5: Fraction.RejectsAZeroDenominator
5/6 Test #5: Fraction.RejectsAZeroDenominator .........   Passed    … sec
    Start 6: Fraction.EqualFractionsHashEqual
6/6 Test #6: Fraction.EqualFractionsHashEqual .........   Passed    … sec

100% tests passed out of 6

Total Test time (real) =   … sec
$ cmake -S . -B build -DFRACTION_INJECT_BUG=ON
[3 lines not shown]
$ cmake --build build
[1/3] Building CXX object CMakeFiles/fraction.dir/src/fraction.cpp.o
[2/3] Linking CXX static library libfraction.a
[3/3] Linking CXX executable fraction_gtest
$ ctest --test-dir build --output-on-failure
Test project build
    Start 1: Fraction.StoresLowestTerms
1/6 Test #1: Fraction.StoresLowestTerms ...............   Passed    … sec
    Start 2: Fraction.KeepsTheSignInTheNumerator
2/6 Test #2: Fraction.KeepsTheSignInTheNumerator ......   Passed    … sec
    Start 3: Fraction.AddsWithDifferentDenominators
3/6 Test #3: Fraction.AddsWithDifferentDenominators ...***Failed    … sec
Running main() from build/_deps/googletest-src/googletest/src/gtest_main.cc
Note: Google Test filter = Fraction.AddsWithDifferentDenominators
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from Fraction
[ RUN      ] Fraction.AddsWithDifferentDenominators
tests/fraction_gtest.cpp:20: Failure
Expected equality of these values:
  Fraction(1, 6) + Fraction(1, 3)
    Which is: 1/1
  Fraction(1, 2)
    Which is: 1/2

[  FAILED  ] Fraction.AddsWithDifferentDenominators (… ms)
[----------] 1 test from Fraction (… ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (… ms total)
[  PASSED  ] 0 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] Fraction.AddsWithDifferentDenominators

 1 FAILED TEST

    Start 4: Fraction.Multiplies
4/6 Test #4: Fraction.Multiplies ......................   Passed    … sec
    Start 5: Fraction.RejectsAZeroDenominator
5/6 Test #5: Fraction.RejectsAZeroDenominator .........   Passed    … sec
    Start 6: Fraction.EqualFractionsHashEqual
6/6 Test #6: Fraction.EqualFractionsHashEqual .........   Passed    … sec

83% tests passed, 1 tests failed out of 6

Total Test time (real) =   … sec

The following tests FAILED:
	  3 - Fraction.AddsWithDifferentDenominators (Failed)
Errors while running CTest
[exit status 8]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 gtest_session.py

The same session as a process, from the six TEST blocks to the exit status CTest ended with, first against the correct library and then against the one with the bug:

Interactive · stage by stage Six tests, from TEST blocks to the verdict of ctest Step through the session. Each step is one stage, with what it printed, first for the correct library and then for the one with the bug.
Six tests, from TEST blocks to the verdict of ctest, step 6 of 6. Showing fraction_gtest.cpp, cmake --build build, CTest's list (6 tests), ctest (5 passed, 1 failed), The verdict (exit status 8). In focus: The verdict.fraction_gtest.cpp6 TEST blockscmake --build build8 stepsCTest's list6 testsctest5 passed, 1 failedThe verdictexit status 81 of 6 failed

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Six behaviours. fraction_gtest.cpp holds six TEST blocks, one behaviour each: lowest terms, the sign in the numerator, addition, multiplication, the zero denominator and equal hashes.
  2. Build. The build ran 8 steps: GoogleTest's two libraries, gtest and gtest_main, which FetchContent had downloaded, the fraction library and the test program fraction_gtest.
  3. Discovery. After the build, gtest_discover_tests asked the program for its tests, and CTest's list now holds six entries, from Fraction.StoresLowestTerms to Fraction.EqualFractionsHashEqual.
  4. Six passes. ctest ran each entry as its own process. Every process exited with status 0, so CTest printed 100% tests passed out of 6.
  5. The bug. With the library rebuilt so that operator+ adds the denominators, test 3 exited with a non-zero status. --output-on-failure printed its report: Fraction(1, 6) + Fraction(1, 3) was 1/1, where 1/2 was expected.
  6. A failed suite. CTest ended with 83% tests passed, 1 tests failed out of 6 and exit status 8, which fails any CI step that runs it.

The first ctest run passed all six tests, each run as its own process. Then the library was rebuilt with a bug in operator+, which adds the denominators instead of multiplying them, and one test failed. With --output-on-failure, CTest printed GoogleTest’s report for that test only: the file and line, the expression Fraction(1, 6) + Fraction(1, 3), the value it produced, 1/1, and the value expected, 1/2. The values are readable because the class has an operator<<; without one, GoogleTest prints the object’s bytes. CTest ended with 83% tests passed, 1 tests failed out of 6 and exit status 8, which fails any CI step that runs it.

Hash Generator Work out the SHA-256 of a downloaded archive before you pin it in URL_HASH, and check it against the one the project publishes.

Writing unit tests with GoogleTest is mostly writing good expectations: one behaviour per test, inputs at the edges (empty, zero, negative, one past a limit), and names that say what failed before you open the report.

Key idea

A test passes when its program exits with status 0. The framework turns a failed check into a report of the values and a non-zero status, and CTest turns any failed test into a failed run that stops CI.

Checkpoint

Check: Tests for Fraction, and a failing run read

  1. Question 1 of 2 With one of the six tests failing, which exit status did ctest end with in the recorded run?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 8

    CTest ended with 83% tests passed, 1 tests failed out of 6, and exit status 8. Any status other than 0 fails the CI step that runs ctest.

  2. Question 2 of 2 The failing test printed "Which is: 1/1" and "Which is: 1/2". What made the values readable?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Fraction has an operator<< that writes it as numerator/denominator

    GoogleTest prints values with operator<< when a type has one, so the report shows 1/1 and 1/2. Without it, GoogleTest falls back to printing the object's bytes, which says little about a fraction.

Check yourself

Eight questions about frameworks, CTest and the failing run above, the least familiar idea first, each answered as soon as you choose.

Practice that adapts to you

Practise this lesson

8 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 What does CTest know about the framework a test was written with?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: Nothing: a CTest test is a command, which passes when it exits with status 0

    CTest is the runner: it keeps the list of every test, runs each one as a command and says which failed, with an exit status a CI job can act on. The framework lives inside the test program.

  2. Question 2 of 8 A test passes most of the time but fails now and then. Which ctest option repeats it to expose the failure?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: --repeat until-fail:50

    ctest -j 8 runs tests in parallel, -R selects them by name and --repeat until-fail:50 repeats them to expose a flaky test; together, -R and --repeat repeat just the suspect one.

  3. Question 3 of 8 What does a SECTION do in a Catch2 test case?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: The test case runs once per section, each time from a fresh copy of its setup code

    Catch2's SECTIONs run one test case once per section, each time from a fresh copy of its setup code, which gives tests shared setup without shared state.

  4. Question 4 of 8 A project already runs GoogleTest tests with ctest in CI. It adds doctest tests for a new module, registered with doctest_discover_tests. What changes in the CI command?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: Nothing: ctest now runs the doctest tests too, as more commands in the same list

    CTest knows nothing about frameworks: gtest_discover_tests, catch_discover_tests and doctest_discover_tests all add commands to the same list, so one ctest run, and one exit status, covers every test.

  5. Question 5 of 8 Which ctest option printed the report of the failing test, and only of that one?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: --output-on-failure

    With --output-on-failure, ctest shows the output of each test that failed and stays quiet about the ones that passed. -N only lists the tests, and -j runs them in parallel.

  6. Question 6 of 8 A GoogleTest test has two checks in a row, and the first one fails. What happens with EXPECT_EQ as the first check, and with ASSERT_EQ?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: EXPECT_EQ reports the failure and goes on to the second check; ASSERT_EQ stops the test

    GoogleTest's checks name what they compare: EXPECT_EQ keeps going after a failure, ASSERT_EQ stops the test, and EXPECT_THROW checks that an exception is thrown.

  7. Question 7 of 8 Why does FetchContent_Declare name GoogleTest's release archive together with URL_HASH SHA256=…?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: So that the configure step fails if the download ever differs from the archive that was pinned

    FetchContent_Declare names the GoogleTest 1.18.0 release archive and its SHA-256, so the configure step fails if the download ever differs, and every build tests with the same framework code.

  8. Question 8 of 8 You add operator- to Fraction. Which new test follows the advice of this lesson best?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: TEST(Fraction, SubtractsToANegativeResult), checking that Fraction(1, 3) - Fraction(1, 2) equals Fraction(-1, 6)

    Writing unit tests is mostly writing good expectations: one behaviour per test, inputs at the edges (empty, zero, negative, one past a limit), and names that say what failed before you open the report.

The same tests in Catch2 and doctest, with their build costs measured Pro: opens with a Pro, Premium or Ultimate pass

The framework you pick shapes how tests read and how long every build takes. Here the Fraction tests are written in Catch2 and doctest too, built side by side, and compared on what the compiler has to do.

In this part:

  • Catch2 and doctest versions of the suite
  • What each framework costs, measured
  • The same failure, three reports

2 runnable examples (C++ (fraction_catch2.cpp), C++ (fraction_doctest.cpp), Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Running the suite under sanitizers: a bug the tests passed over, caught Pro: opens with a Pro, Premium or Ultimate pass

A test can pass while the code it runs has undefined behaviour, because the program happened to compute the expected value. Running the same tests in a sanitizer build turns that luck into a failure, if the sanitizers are set up to fail the test.

In this part:

  • Test presets for sanitizer builds
  • A passing suite with a hidden report
  • Why the hidden report matters
  • Settings worth copying into CI

1 diagram · 2 runnable examples (JSON, Python) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Tests that find bugs: the StringUtils exercise, solved, with a rubric Pro: opens with a Pro, Premium or Ultimate pass

Good tests come from asking where a function is most likely to be wrong. This exercise gives a small string library with four bugs; the tests below find all four, one failing test each.

In this part:

  • The exercise
  • One solution: thirteen tests, four failures
  • Where the four tests came from
  • A rubric for a unit-test suite

3 runnable examples (C++, C++ header, Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher · Warm-up

    What is the difference between CTest and a test framework such as GoogleTest or Catch2?

    A test framework is a library you write tests with: it provides the macros such as TEST and EXPECT_EQ or TEST_CASE and CHECK, runs the checks inside one test program and prints a report of the values when a check fails. CTest is the runner that comes with CMake. It knows nothing about frameworks: a CTest test is a command, which passes when it exits with status 0. CMake connects the two: gtest_discover_tests, catch_discover_tests or doctest_discover_tests ask the built test program for its tests and add one CTest test for each. CTest then runs them, in parallel with -j or selected with -R, shows the output of failures with --output-on-failure, and ends with a non-zero exit status when any test failed, which is what fails a CI job.

  2. Mid to senior

    A GoogleTest test passes on every developer's machine but fails about once in 50 CI runs. Using CTest and the framework's own options, how do you find out whether the test or the code under test is at fault?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Senior to staff

    A C++ suite of about 3,000 GoogleTest tests in 300 files takes 14 minutes in CI, most of it in the build. How would you make it faster without testing less?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A function decides whether a cached file is stale by comparing its modification time with the current time. How do you unit-test it reliably, without sleeping and without depending on the real clock or disk?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A framework (GoogleTest, Catch2, doctest) writes and checks tests; CTest runs them and reports through its exit status.
  • Pin the framework with FetchContent_Declare(... URL ... URL_HASH SHA256=...) and register tests with gtest_discover_tests, so each test becomes its own CTest entry.
  • ctest --output-on-failure shows the report of each failing test: the expression, the value it had and the value expected.
  • Test one behaviour per test, at the edges of the input, and give it a name that says what broke.

Terms in this lesson

test discovery
Asking a built test program for its list of tests, as gtest_discover_tests does after each build, so that each test becomes a CTest test of its own that can be run, selected and timed alone.
test framework
A library you write tests with, such as GoogleTest, Catch2 or doctest. It provides the macros for tests and checks, runs them inside one test program and reports the values when a check fails.
unit test
A small program that calls one piece of code, a function or a class, with chosen inputs and checks what it returns, so that a wrong result fails the test instead of reaching a user.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.