Google Test (gtest) Tutorial: Unit Testing in C++

Google Test (gtest) Tutorial: Unit Testing in C++

Google Test (gtest) is the de facto standard for C++ unit testing. It ships with a comprehensive assertion library, test fixtures, parameterized tests, and death tests. This tutorial gets you from zero to a working test suite integrated with CMake and CI in under an hour.


Why Google Test

C++ has many testing frameworks, but Google Test dominates for good reasons:

  • Battle-tested at scale — used in Chromium, LLVM, and thousands of production codebases
  • Rich assertions — value matchers, predicate assertions, death tests, custom matchers
  • CMake integration — works seamlessly with modern CMake via FetchContent
  • Active development — maintained by Google with frequent releases
  • GMock bundled — mocking library included, no separate install

Setup

# CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)

# Fetch Google Test
include(FetchContent)
FetchContent_Declare(
    googletest
    URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip
)
FetchContent_MakeAvailable(googletest)

# Your library
add_library(mylib src/calculator.cpp)
target_include_directories(mylib PUBLIC include/)

# Test executable
add_executable(mylib_tests
    tests/test_calculator.cpp
    tests/test_string_utils.cpp
)
target_link_libraries(mylib_tests
    PRIVATE mylib
    PRIVATE GTest::gtest_main
)

# Register with CTest
include(GoogleTest)
gtest_discover_tests(mylib_tests)

Manual Install

# Ubuntu/Debian
<span class="hljs-built_in">sudo apt install libgtest-dev cmake
<span class="hljs-built_in">cd /usr/src/gtest && <span class="hljs-built_in">sudo cmake . && <span class="hljs-built_in">sudo make && <span class="hljs-built_in">sudo <span class="hljs-built_in">cp *.a /usr/lib

<span class="hljs-comment"># macOS
brew install googletest

<span class="hljs-comment"># vcpkg
vcpkg install gtest

Your First Test

// include/calculator.h
class Calculator {
public:
    int add(int a, int b);
    int divide(int a, int b);
    bool is_prime(int n);
};
// tests/test_calculator.cpp
#include <gtest/gtest.h>
#include "calculator.h"

// Basic test: TEST(TestSuiteName, TestName)
TEST(CalculatorTest, AddsTwoIntegers) {
    Calculator calc;
    EXPECT_EQ(calc.add(2, 3), 5);
}

TEST(CalculatorTest, AddNegativeNumbers) {
    Calculator calc;
    EXPECT_EQ(calc.add(-1, -2), -3);
}

TEST(CalculatorTest, AddZero) {
    Calculator calc;
    EXPECT_EQ(calc.add(42, 0), 42);
}

Build and run:

cmake -B build && cmake --build build
./build/mylib_tests

Output:

[==========] Running 3 tests from 1 test suite.
[----------] 3 tests from CalculatorTest
[ RUN      ] CalculatorTest.AddsTwoIntegers
[       OK ] CalculatorTest.AddsTwoIntegers (0 ms)
[ RUN      ] CalculatorTest.AddNegativeNumbers
[       OK ] CalculatorTest.AddNegativeNumbers (0 ms)
[ RUN      ] CalculatorTest.AddZero
[       OK ] CalculatorTest.AddZero (0 ms)
[----------] 3 tests from CalculatorTest (0 ms total)

[==========] 3 tests ran.
[  PASSED  ] 3 tests.

Assertion Reference

Google Test has two assertion families:

  • ASSERT_* — fatal: stops the current test immediately on failure
  • EXPECT_* — non-fatal: records failure but continues

Use EXPECT_ by default; use ASSERT_ when continuing after failure would crash or produce meaningless results.

Equality and Comparison

EXPECT_EQ(val1, val2);   // val1 == val2
EXPECT_NE(val1, val2);   // val1 != val2
EXPECT_LT(val1, val2);   // val1 < val2
EXPECT_LE(val1, val2);   // val1 <= val2
EXPECT_GT(val1, val2);   // val1 > val2
EXPECT_GE(val1, val2);   // val1 >= val2

Boolean

EXPECT_TRUE(condition);
EXPECT_FALSE(condition);

Floating Point

EXPECT_FLOAT_EQ(val1, val2);    // within 4 ULPs
EXPECT_DOUBLE_EQ(val1, val2);   // within 4 ULPs
EXPECT_NEAR(val1, val2, abs_error);  // |val1 - val2| <= abs_error

// Example
EXPECT_NEAR(3.14159, M_PI, 0.00001);

Strings

EXPECT_STREQ(str1, str2);    // C strings: strcmp == 0
EXPECT_STRNE(str1, str2);    // C strings: strcmp != 0
EXPECT_STRCASEEQ(str1, str2); // case-insensitive
EXPECT_STRCASENE(str1, str2);

// For std::string, use EXPECT_EQ
std::string result = get_greeting("Alice");
EXPECT_EQ(result, "Hello, Alice!");

Exceptions

EXPECT_THROW(calc.divide(1, 0), std::invalid_argument);
EXPECT_NO_THROW(calc.divide(10, 2));
EXPECT_ANY_THROW(risky_function());

Test Fixtures

Fixtures eliminate setup/teardown boilerplate by sharing state across tests in a suite:

// tests/test_calculator.cpp
class CalculatorFixture : public ::testing::Test {
protected:
    void SetUp() override {
        // Called before each TEST_F
        calc = std::make_unique<Calculator>();
        calc->set_precision(2);
    }
    
    void TearDown() override {
        // Called after each TEST_F — cleanup happens here
        // (unique_ptr handles deallocation automatically)
    }
    
    std::unique_ptr<Calculator> calc;
};

// Use TEST_F instead of TEST when using a fixture
TEST_F(CalculatorFixture, AddWorks) {
    EXPECT_EQ(calc->add(2, 3), 5);
}

TEST_F(CalculatorFixture, DivisionWithPrecision) {
    EXPECT_NEAR(calc->divide(1, 3), 0.33, 0.01);
}

TEST_F(CalculatorFixture, DivisionByZeroThrows) {
    EXPECT_THROW(calc->divide(1, 0), std::invalid_argument);
}

Each TEST_F gets a fresh CalculatorSetUp() runs before each test, TearDown() runs after.


Parameterized Tests

Run the same test logic with different inputs:

class IsPrimeTest : public ::testing::TestWithParam<int> {};

TEST_P(IsPrimeTest, ReturnsTrue) {
    Calculator calc;
    EXPECT_TRUE(calc.is_prime(GetParam()));
}

INSTANTIATE_TEST_SUITE_P(
    PrimeNumbers,
    IsPrimeTest,
    ::testing::Values(2, 3, 5, 7, 11, 13, 17, 19, 23)
);

Output:

[ RUN      ] PrimeNumbers/IsPrimeTest.ReturnsTrue/0  (2)  OK
[ RUN      ] PrimeNumbers/IsPrimeTest.ReturnsTrue/1  (3)  OK
[ RUN      ] PrimeNumbers/IsPrimeTest.ReturnsTrue/2  (5)  OK
...

Parameterized with Structs

struct DivisionCase {
    int a, b, expected;
};

class DivisionTest : public ::testing::TestWithParam<DivisionCase> {};

TEST_P(DivisionTest, ComputesCorrectly) {
    auto [a, b, expected] = GetParam();
    Calculator calc;
    EXPECT_EQ(calc.divide(a, b), expected);
}

INSTANTIATE_TEST_SUITE_P(
    DivisionCases,
    DivisionTest,
    ::testing::Values(
        DivisionCase{10, 2, 5},
        DivisionCase{9, 3, 3},
        DivisionCase{100, 4, 25},
        DivisionCase{7, 2, 3}  // integer division
    )
);

Death Tests

Test that code terminates with the expected exit code or signal:

// Test that an assertion fires (using NDEBUG-off builds)
TEST(DeathTest, DivideByZeroCrashes) {
    Calculator calc;
    // EXPECT_DEATH runs in a subprocess
    EXPECT_DEATH(calc.unsafe_divide(1, 0), "Assertion.*failed");
}

// Test exit codes
TEST(DeathTest, ExitsWithCode1OnError) {
    EXPECT_EXIT(error_function(), ::testing::ExitedWithCode(1), "");
}

// Test signal
TEST(DeathTest, SegfaultOnNullDeref) {
    EXPECT_DEATH(
        {
            int* p = nullptr;
            *p = 1;
        },
        ""
    );
}

Death tests run in a subprocess so they don't crash your test runner.


Custom Assertions

Wrap repeated assertion logic:

// In a test utility header
::testing::AssertionResult IsEven(int n) {
    if (n % 2 == 0) {
        return ::testing::AssertionSuccess();
    }
    return ::testing::AssertionFailure() << n << " is odd, expected even";
}

TEST(MathTest, ResultIsEven) {
    EXPECT_TRUE(IsEven(calc.compute_result()));
    // On failure: "n is odd, expected even"
}

Running Subsets of Tests

# Run only tests matching a pattern
./mylib_tests --gtest_filter=CalculatorTest.*

<span class="hljs-comment"># Run all tests except death tests
./mylib_tests --gtest_filter=-*DeathTest*

<span class="hljs-comment"># Run a specific test
./mylib_tests --gtest_filter=CalculatorTest.AddsTwoIntegers

<span class="hljs-comment"># Repeat tests (catch intermittent failures)
./mylib_tests --gtest_repeat=10

<span class="hljs-comment"># Shuffle test order (find order-dependent bugs)
./mylib_tests --gtest_shuffle

<span class="hljs-comment"># Generate XML report for CI
./mylib_tests --gtest_output=xml:results.xml

CMake + CTest Integration

# Enable CTest support
enable_testing()

# After gtest_discover_tests(mylib_tests):
# Now you can run tests via CTest
# Build
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build

<span class="hljs-comment"># Run via CTest
<span class="hljs-built_in">cd build && ctest --output-on-failure

<span class="hljs-comment"># Run in parallel
ctest -j4 --output-on-failure

<span class="hljs-comment"># Generate coverage (requires gcov)
ctest -T Coverage

GitHub Actions CI

name: C++ Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install dependencies
        run: sudo apt install -y cmake ninja-build
      
      - name: Configure
        run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
      
      - name: Build
        run: cmake --build build
      
      - name: Test
        run: cd build && ctest --output-on-failure -j4
      
      - name: Upload test results
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: build/Testing/

Test Organization Best Practices

1. One test file per source file

src/calculator.cpp       → tests/test_calculator.cpp
src/string_utils.cpp     → tests/test_string_utils.cpp

2. Descriptive test names that document behavior

// Bad
TEST(Calc, Test1) { ... }

// Good
TEST(CalculatorTest, AddReturnsCorrectSumForPositiveIntegers) { ... }
TEST(CalculatorTest, DivideByZeroThrowsInvalidArgument) { ... }

3. Arrange-Act-Assert structure

TEST_F(CalculatorFixture, DivisionRoundsDown) {
    // Arrange
    int dividend = 7;
    int divisor = 2;
    
    // Act
    int result = calc->divide(dividend, divisor);
    
    // Assert
    EXPECT_EQ(result, 3);
}

4. Test failure paths explicitly

// Don't just test happy paths
TEST(ValidatorTest, RejectsEmptyInput) {
    EXPECT_THROW(Validator::validate(""), ValidationError);
}

TEST(ValidatorTest, RejectsInputLongerThan255Chars) {
    std::string long_input(256, 'a');
    EXPECT_THROW(Validator::validate(long_input), ValidationError);
}

Next Steps

  • Add GMock for mocking — it's bundled with gtest, see the GMock guide
  • Set up coverage with gcov + lcov to find untested code paths
  • Try Catch2 for a more modern C++ testing style with expression-based assertions
  • Run with AddressSanitizer to catch memory errors — see memory testing in C/C++

Read more