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
CMake + FetchContent (Recommended)
# 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 gtestYour 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_testsOutput:
[==========] 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 failureEXPECT_*— 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 >= val2Boolean
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 Calculator — SetUp() 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.xmlCMake + 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 CoverageGitHub 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.cpp2. 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++