Skip to content
C Codeloom
C++

C++ Unit Testing with GoogleTest

Set up GoogleTest in a CMake project and write clear unit tests with assertions, fixtures, and parameterized tests for confident C++ code.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • Add GoogleTest to a CMake project with FetchContent
  • Write TEST and TEST_F cases with assertions
  • Use fixtures to share setup across tests
  • Apply parameterized tests for data-driven cases
  • Integrate tests into your build with ctest

Prerequisites

  • CMake basics — see /blog/cpp-cmake-tutorial

GoogleTest is the most widely used C++ unit testing framework. It is straightforward to integrate, has rich assertions, and plays well with CMake and CI. This article walks through setting it up and writing your first useful tests.

What and Why

Unit tests give you a safety net for refactoring and a precise specification of behavior. In C++, where mistakes can corrupt memory in distant code, tests are doubly valuable. GoogleTest provides assertion macros, automatic test discovery, fixtures for shared setup, parameterization, and a friendly runner.

The alternative — printf-style scripts or no tests at all — leaves regressions invisible until a user finds them.

Mental Model

A test is a function that exercises code and asserts properties. GoogleTest collects every TEST macro into a global registry. When you run the test binary, it iterates through them, reports pass or fail, and exits with a non-zero code on failure. That non-zero exit code is what CI hooks into.

Fixtures (TEST_F) extend this with a class that runs before and after each test. Parameterized tests (TEST_P) run the same logic across a set of inputs.

Hands-on Example

Add GoogleTest to CMake and write a small test suite.

include(FetchContent)
FetchContent_Declare(googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG v1.14.0)
FetchContent_MakeAvailable(googletest)

enable_testing()
add_executable(tests tests/calc_test.cpp src/calc.cpp)
target_include_directories(tests PRIVATE include)
target_link_libraries(tests PRIVATE GTest::gtest_main)

include(GoogleTest)
gtest_discover_tests(tests)
#include <gtest/gtest.h>
#include "calc.hpp"

TEST(CalcTest, AddsPositives) {
    EXPECT_EQ(add(2, 3), 5);
}

TEST(CalcTest, HandlesZero) {
    EXPECT_EQ(add(0, 7), 7);
}

class CalcFixture : public ::testing::Test {
protected:
    void SetUp() override { /* shared setup */ }
};

TEST_F(CalcFixture, Negatives) {
    EXPECT_EQ(add(-2, -3), -5);
}
  main(gtest_main)
      │
      v
discover TEST macros ──> registry
      │
      v
for each test:
   SetUp() ──> body ──> TearDown() ──> record pass/fail
      │
      v
print summary, exit with status code
GoogleTest flow: discover, set up fixture, run test, tear down, report.

Run cmake --build build && ctest --test-dir build to see pass/fail. Each failing assertion prints the file, line, and values involved.

Common Pitfalls

A common mistake is using ASSERT_* everywhere when EXPECT_* is enough. ASSERT_* returns from the current function on failure, useful when later lines would crash. EXPECT_* records failure and continues, giving you more information per run.

Another pitfall is non-deterministic tests. Tests that depend on timing, the network, or random seeds give flaky failures. Inject clocks and RNGs through interfaces, and seed them deterministically in tests.

Sharing state across tests via global variables creates order dependencies. GoogleTest may run tests in any order; rely on fixtures, not globals.

Forgetting gtest_discover_tests(tests) means CTest will only see one entry instead of one per test case. The dashboard becomes useless.

Practical Tips

Name tests with the pattern Subject_Action_ExpectedResult to make failures read like English in CI logs.

Use EXPECT_THAT with matchers (from GoogleMock) for richer assertions: EXPECT_THAT(v, ElementsAre(1, 2, 3)) is clearer than three EXPECT_EQ lines.

Keep tests fast. A second-long unit test discourages contributors from running the suite. Reserve slow tests for an integration target.

Use parameterized tests when the same logic must hold for many inputs:

class Squared : public ::testing::TestWithParam<int> {};
TEST_P(Squared, IsNonNegative) {
    EXPECT_GE(GetParam() * GetParam(), 0);
}
INSTANTIATE_TEST_SUITE_P(Many, Squared, ::testing::Values(-3, 0, 5));

Hook the test target into CI so every push runs ctest --output-on-failure. A green build is the daily proof that nothing obvious is broken.

Wrap-up

GoogleTest plus CMake plus CTest is the standard modern C++ testing stack. Pull it in with FetchContent, write small TEST cases with informative names, use fixtures to share setup, and lean on parameterization for data-driven coverage. Once tests run on every push, refactoring becomes far less scary and bugs are caught long before users see them.