What is GoogleTest (gtest)
GoogleTest is a C++ testing framework that gives you:
- A test runner (discovers and runs tests)
- Assertions (
EXPECT_,ASSERT_) to verify behavior - Test fixtures to share setup/teardown
- Parameterized tests, typed tests, death tests, and more
In most projects you’ll also use GoogleMock (gmock) (often shipped alongside gtest) for mocking dependencies.
Install & Build Setup (CMake)
Recommended: FetchContent (no system install required)
Create a CMakeLists.txt like this:
cmake_minimum_required(VERSION 3.20)
project(MyProjectTests LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip
)
# For Windows: prevent overriding parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
add_library(my_lib
src/math.cpp
include/math.hpp
)
target_include_directories(my_lib PUBLIC include)
add_executable(my_tests
tests/test_math.cpp
)
target_link_libraries(my_tests PRIVATE my_lib GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(my_tests)
Build + run
cmake -S . -B build
cmake --build build -j
ctest --test-dir build --output-on-failure
Your First Test: `TEST` + Assertions
Let’s say you have:
include/math.hpp
#pragma once
int add(int a, int b);
int divide(int a, int b); // throws on b==0
src/math.cpp
#include "math.hpp"
#include <stdexcept>
int add(int a, int b) { return a + b; }
int divide(int a, int b) {
if (b == 0) throw std::invalid_argument("division by zero");
return a / b;
}
tests/test_math.cpp
#include <gtest/gtest.h>
#include "math.hpp"
TEST(Math, Add_Works) {
EXPECT_EQ(add(2, 3), 5);
EXPECT_EQ(add(-2, 2), 0);
}
TEST(Math, Divide_Works) {
EXPECT_EQ(divide(10, 2), 5);
}
TEST(Math, Divide_ByZero_Throws) {
EXPECT_THROW(divide(10, 0), std::invalid_argument);
}
Key idea: `EXPECT_*` vs `ASSERT_*`
EXPECT_*= record failure and continue the testASSERT_*= record failure and stop the test immediately
Example:
TEST(Parser, AssertExample) {
auto result = /* parse something */;
ASSERT_TRUE(result.has_value()); // if false, test ends here
EXPECT_EQ(result->field, 123); // safe because result exists
}
Use ASSERT_* when continuing would cause crashes or misleading errors.
Common Assertions You’ll Use Constantly
Equality / comparisons
EXPECT_EQ(a, b);
EXPECT_NE(a, b);
EXPECT_LT(a, b);
EXPECT_LE(a, b);
EXPECT_GT(a, b);
EXPECT_GE(a, b);
Boolean
EXPECT_TRUE(cond);
EXPECT_FALSE(cond);
Floating-point (important)
Don’t do EXPECT_EQ for floats:
EXPECT_NEAR(value, expected, 1e-6);
Strings:
EXPECT_STREQ(c_str1, c_str2); // C strings
EXPECT_EQ(std::string("a"), s); // std::string
Exceptions
EXPECT_THROW(fn(), std::runtime_error);
EXPECT_NO_THROW(fn());
EXPECT_ANY_THROW(fn());
Test Fixtures: Shared Setup/Teardown with TEST_F
When multiple tests need the same setup, use a fixture.
Example: testing a small BankAccount.
include/bank.hpp
#pragma once
#include <stdexcept>
class BankAccount {
public:
explicit BankAccount(int initial) : balance_(initial) {}
void deposit(int amount) {
if (amount < 0) throw std::invalid_argument("negative deposit");
balance_ += amount;
}
void withdraw(int amount) {
if (amount < 0) throw std::invalid_argument("negative withdraw");
if (amount > balance_) throw std::runtime_error("insufficient funds");
balance_ -= amount;
}
int balance() const { return balance_; }
private:
int balance_;
};
tests/test_bank.cpp
#include <gtest/gtest.h>
#include "bank.hpp"
class BankFixture : public ::testing::Test {
protected:
BankAccount acc{100}; // runs before each test (fresh instance)
};
TEST_F(BankFixture, Deposit_IncreasesBalance) {
acc.deposit(50);
EXPECT_EQ(acc.balance(), 150);
}
TEST_F(BankFixture, Withdraw_DecreasesBalance) {
acc.withdraw(40);
EXPECT_EQ(acc.balance(), 60);
}
TEST_F(BankFixture, Withdraw_TooMuch_Throws) {
EXPECT_THROW(acc.withdraw(200), std::runtime_error);
}
Fixture lifecycle
- A new fixture instance is created per test
- Great for isolation (tests don’t affect each other)
Parameterized Tests (Stop Copy-Pasting)
If you’re testing many input/output pairs, use parameterized tests.
#include <gtest/gtest.h>
#include "math.hpp"
#include <tuple>
class AddParams : public ::testing::TestWithParam<std::tuple<int,int,int>> {};
TEST_P(AddParams, WorksForManyCases) {
auto [a, b, expected] = GetParam();
EXPECT_EQ(add(a, b), expected);
}
INSTANTIATE_TEST_SUITE_P(
AddCases,
AddParams,
::testing::Values(
std::make_tuple(1, 2, 3),
std::make_tuple(-1, 1, 0),
std::make_tuple(10, 5, 15)
)
);
Now you get separate test instances, each with a clear name.
Better Failure Messages
Stream extra info
EXPECT_EQ(actual, expected) << "when input=" << input;
`SCOPED_TRACE` (super helpful in loops)
for (int i = 0; i < 10; ++i) {
SCOPED_TRACE(::testing::Message() << "i=" << i);
EXPECT_TRUE(check(i));
}
When it fails, you see which i caused it.
Testing “Hard-to-Test” Code: Design for Testability
A lot of C++ pain comes from code glued to:
- filesystem
- network
- time (
std::chrono::system_clock::now()) - randomness
- hardware APIs
If a unit test needs real disk or real network, it stops being a _unit_ test.
Example: Inject time (instead of calling `now()` inside)
using NowFn = std::function<std::chrono::system_clock::time_point()>;
class Token {
public:
explicit Token(NowFn now) : now_(std::move(now)) {}
bool expired(std::chrono::seconds ttl) const {
return (now_() - created_) > ttl;
}
private:
NowFn now_;
std::chrono::system_clock::time_point created_{std::chrono::system_clock::now()};
};
In test you pass a deterministic now().
GoogleMock (gmock) Quick Practical Example
When your code depends on an interface, mock it.
Production code
#pragma once
#include <string>
struct ILogger {
virtual ~ILogger() = default;
virtual void info(const std::string& msg) = 0;
virtual void error(const std::string& msg) = 0;
};
class Worker {
public:
explicit Worker(ILogger& log) : log_(log) {}
bool run(int x) {
if (x < 0) {
log_.error("x is negative");
return false;
}
log_.info("running");
return true;
}
private:
ILogger& log_;
};
Test with gmock
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "worker.hpp"
class MockLogger : public ILogger {
public:
MOCK_METHOD(void, info, (const std::string&), (override));
MOCK_METHOD(void, error, (const std::string&), (override));
};
TEST(Worker, LogsErrorOnNegative) {
MockLogger log;
Worker w(log);
EXPECT_CALL(log, error(testing::HasSubstr("negative"))).Times(1);
EXPECT_CALL(log, info(testing::_)).Times(0);
EXPECT_FALSE(w.run(-1));
}
TEST(Worker, LogsInfoOnSuccess) {
MockLogger log;
Worker w(log);
EXPECT_CALL(log, info("running")).Times(1);
EXPECT_CALL(log, error(testing::_)).Times(0);
EXPECT_TRUE(w.run(5));
}
To link gmock in CMake:
target_link_libraries(my_tests PRIVATE GTest::gtest_main GTest::gmock)
Test Naming & Structure Conventions That Scale
Naming
Use intent-based names:
Divide_ByZero_ThrowsParse_EmptyString_ReturnsErrorEncoder_InvalidHeader_Rejects
Arrange / Act / Assert (AAA)
Keep tests readable:
TEST(X, Y) {
// Arrange
Foo f{...};
// Act
auto r = f.doThing();
// Assert
EXPECT_TRUE(r.ok());
}
One reason to fail
Try to test one behavior per test. If it fails, you know what broke.
Running Tests & Filtering
Run all:
./build/my_tests
List tests:
./build/my_tests --gtest_list_tests
Run a subset:
./build/my_tests --gtest_filter=Math.*
./build/my_tests --gtest_filter=*Divide*
Repeat until failure (useful for flaky tests):
./build/my_tests --gtest_repeat=100 --gtest_break_on_failure
Death Tests (Advanced)
Death tests verify code terminates (asserts, aborts, etc.). Mostly useful for validating invariants.
TEST(Fatal, DiesOnBadInvariant) {
EXPECT_DEATH({
// code that should abort/assert
}, ".*");
}
Use sparingly—designing code to return errors is usually better.
Unit Tests vs Integration Tests (Don’t Mix Them)
- Unit test: fast, deterministic, no external dependencies
- Integration test: real filesystem/network/db/hardware, slower, validates wiring
GoogleTest can run both, but keep them in separate targets or label/filter them.
A “Real” Example: Testing a Parser Function
Here’s a slightly more realistic scenario: parse a config line.
include/config.hpp
#pragma once
#include <optional>
#include <string>
struct Kv { std::string key; std::string value; };
// Parses "key=value" (no spaces). Returns nullopt if invalid.
std::optional<Kv> parse_kv(std::string_view line);
src/config.cpp
#include "config.hpp"
std::optional<Kv> parse_kv(std::string_view line) {
auto pos = line.find('=');
if (pos == std::string_view::npos) return std::nullopt;
if (pos == 0 || pos == line.size() - 1) return std::nullopt;
Kv kv;
kv.key = std::string(line.substr(0, pos));
kv.value = std::string(line.substr(pos + 1));
return kv;
}
tests/test_config.cpp
#include <gtest/gtest.h>
#include "config.hpp"
TEST(Config, ParseValid) {
auto kv = parse_kv("mode=fast");
ASSERT_TRUE(kv.has_value());
EXPECT_EQ(kv->key, "mode");
EXPECT_EQ(kv->value, "fast");
}
TEST(Config, MissingEquals_Invalid) {
EXPECT_FALSE(parse_kv("modefast").has_value());
}
TEST(Config, EmptyKey_Invalid) {
EXPECT_FALSE(parse_kv("=fast").has_value());
}
TEST(Config, EmptyValue_Invalid) {
EXPECT_FALSE(parse_kv("mode=").has_value());
}
This is the kind of testing that pays off immediately.
Best Practices (The Ones That Actually Matter)
- Keep tests fast (milliseconds per file, not seconds)
- Avoid global/shared state
- Don’t use randomness unless you seed deterministically
- Prefer pure functions and dependency injection
- Use
ASSERT_*before dereferencing pointers/optionals - Make tests readable like documentation
- Treat flaky tests as broken tests (fix or delete)
Mini Checklist: When your gtest suite is “good”
You’re in a great place when:
- Running the full suite takes under a minute
- Tests fail with clear messages
- Changing implementation doesn’t break tests unless behavior changes
- You can refactor without fear