GoogleTest and gMock in Practice: ASSERT vs EXPECT, Fixtures, Mocks and CTest
A unit test suite earns its keep on the day someone refactors a function and a different function breaks. This article covers GoogleTest (gtest) and its mocking half, gMock, with an emphasis on the semantics that cause confusion: what a failed assertion actually does to control flow, when a fixture is constructed, how gMock picks an expectation, and why a test can pass under CTest yet fail when you run the binary yourself.
Everything below was built with g++ 10.3, -std=c++17, against GoogleTest v1.14.0, and every test output quoted here comes from actually running it. GoogleTest’s minimum language standard has moved over time: the 1.12.x releases were the last to support C++11, the 1.13 to 1.16 releases require C++14, and 1.17 requires C++17. Pin a release and check its notes against your compiler.
CMake integration: FetchContent and gtest_discover_tests
The least surprising setup is to let CMake download a pinned GoogleTest release and build it with your own compiler flags:
cmake_minimum_required(VERSION 3.14)
project(demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
FetchContent_Declare(googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip)
# MSVC: use the same runtime library (/MD vs /MT) as the parent project
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
add_executable(unit_tests split_test.cpp api_test.cpp)
target_link_libraries(unit_tests PRIVATE mylib GTest::gtest_main GTest::gmock)
include(GoogleTest)
gtest_discover_tests(unit_tests)
Why these choices:
- Build from source, not a prebuilt package. GoogleTest has no stable ABI across compilers and flag sets. Linking a system
libgtestbuilt with different flags (a different-std,_GLIBCXX_DEBUG, or a different MSVC runtime) produces link errors or subtle runtime failures. FetchContent builds it inside your tree with your settings. vcpkg and Conan also work, because they build it for your triplet; a distribution package is the one to be wary of. - A release archive, not a moving branch.
GIT_TAG mainmeans two CI runs a week apart can test against different GoogleTest code. gtest_force_shared_crtmatters only on MSVC, where GoogleTest otherwise defaults to the static runtime and conflicts with a parent project that uses/MD.gtest_discover_testsovergtest_add_tests.gtest_add_testsscans source files with a regex at configure time, so it cannot see the generated names of parameterized and typed tests and has to be re-run whenever the sources change.gtest_discover_testsruns the built binary with--gtest_list_testsafter linking and registers exactly what the binary contains, including every parameterized instance.
GTest::gtest_main provides main() for you. If you link GTest::gtest instead and forget to write one, the linker complains about a missing entry point: undefined reference to 'main' on Linux, and on MinGW it is undefined reference to 'WinMain'. The reverse mistake, linking gtest_main and also defining main, gives a multiple-definition error.
Run the suite with ctest --output-on-failure (add -j for parallelism). For CI dashboards, the test binary can write JUnit-style XML with --gtest_output=xml:report.xml.
ASSERT vs EXPECT: what a failure does
Both families check the same conditions. The difference is control flow:
EXPECT_*records a non-fatal failure and the test keeps running, so one run reports every broken check.ASSERT_*records a fatal failure and then executesreturn;from the current function.
Use ASSERT when the lines after it would be meaningless or unsafe if the check failed, for example indexing into a vector whose size you have not confirmed:
#include <gtest/gtest.h>
#include <sstream>
#include <string>
#include <vector>
std::vector<std::string> split(const std::string& s, char delim) {
std::vector<std::string> out;
std::stringstream ss(s);
std::string item;
while (std::getline(ss, item, delim)) out.push_back(item);
return out;
}
TEST(SplitTest, AssertGuardsIndexing) {
auto parts = split("a,b", ',');
ASSERT_EQ(parts.size(), 3u); // fails: the next line never runs
EXPECT_EQ(parts[2], "c"); // would be out of bounds
}
TEST(SplitTest, EdgeCases) {
EXPECT_TRUE(split("", ',').empty()); // getline fails immediately: zero parts
EXPECT_EQ(split("a,", ',').size(), 1u); // trailing delimiter: no empty last part
EXPECT_EQ(split(",a", ',').size(), 2u); // leading delimiter: empty first part
}
The failure message shows both expressions and their values:
split_test.cpp:20: Failure
Expected equality of these values:
parts.size()
Which is: 2
3u
Which is: 3
The edge-case test is the kind worth writing before trusting a function like split. It is easy to assume split("") returns one empty string, but std::getline on an empty stream fails straight away and returns nothing. A test pins down which behavior you actually have. Note the 3u literals too: comparing size() with a plain 3 triggers -Wsign-compare warnings inside the macro expansion.
The “return from the current function” consequences
Because ASSERT_* expands to a bare return;, it only compiles in functions that return void:
int parse_or_fail(const char* s) {
ASSERT_NE(s, nullptr); // g++: error: void value not ignored as it ought to be
return 1;
}
The error points into gtest-internal.h, which makes it confusing the first time. A second trap is subtler: an ASSERT inside a void helper returns from the helper, not from the test, so the test keeps going after a fatal failure. Wrap the call with ASSERT_NO_FATAL_FAILURE(helper());, or check HasFatalFailure() afterwards, when the rest of the test depends on the helper succeeding.
Comparison pitfalls
EXPECT_EQon twoconst char*compares pointers. UseEXPECT_STREQfor C strings. Comparing astd::stringwith aconst char*throughEXPECT_EQis fine becausestd::string’soperator==is used.EXPECT_DOUBLE_EQ(a, b)passes when the values are within 4 ULPs, soEXPECT_DOUBLE_EQ(0.1 + 0.2, 0.3)passes. For values computed through longer chains, useEXPECT_NEAR(a, b, tolerance)with a tolerance you can justify.EXPECT_THROW(stmt, Type)checks the exception type only. To check the message, catch it yourself or use gMock’sThrowsMessagematcher.- Add context to any assertion by streaming into it:
EXPECT_EQ(got, want) << "input=" << input;.
Fixtures: a fresh object for every test
TEST_F tests share setup code through a fixture class, but not fixture state. GoogleTest constructs a new fixture object for each test. This instrumented fixture shows the order:
class CounterTest : public ::testing::Test {
protected:
static void SetUpTestSuite() { std::puts("SetUpTestSuite"); }
static void TearDownTestSuite() { std::puts("TearDownTestSuite"); }
CounterTest() { std::puts(" ctor"); }
void SetUp() override { std::puts(" SetUp"); }
void TearDown() override { std::puts(" TearDown"); }
~CounterTest() override { std::puts(" dtor"); }
std::vector<int> data_{1, 2, 3};
};
TEST_F(CounterTest, FreshFixtureA) { data_.push_back(4); EXPECT_EQ(data_.size(), 4u); }
TEST_F(CounterTest, FreshFixtureB) { EXPECT_EQ(data_.size(), 3u); } // passes: A's push_back is gone
SetUpTestSuite
[ RUN ] CounterTest.FreshFixtureA
ctor
SetUp
TearDown
dtor
[ OK ] CounterTest.FreshFixtureA (0 ms)
[ RUN ] CounterTest.FreshFixtureB
ctor
SetUp
TearDown
dtor
[ OK ] CounterTest.FreshFixtureB (0 ms)
TearDownTestSuite
Constructor or SetUp? Prefer the constructor and member initializers for plain initialization, since that lets members be const and references. Use SetUp when initialization needs assertions (a fatal assertion in a constructor only returns from the constructor) or calls virtual functions. Use TearDown for cleanup that may throw or assert; destructors should do neither.
SetUpTestSuite runs once for all tests in the suite and works through static members. It is meant for resources that are expensive and effectively read-only, such as loading a large fixture file. Every test that mutates a suite-level resource makes the suite order-dependent, which leads directly to the next section.
Test isolation and static state
Tests in one binary share one process, so anything static survives from test to test: singletons, function-local statics, global registries and caches.
class Counter {
public:
static int& instances() { static int n = 0; return n; }
Counter() { ++instances(); }
};
TEST(StaticState, First) { Counter c; EXPECT_EQ(Counter::instances(), 1); }
TEST(StaticState, Second) { Counter c; EXPECT_EQ(Counter::instances(), 1); }
Running the test binary directly, Second fails with Counter::instances() at 2. With --gtest_filter=StaticState.Second alone, it passes. And with the CMake setup above, ctest reports it as passing, because gtest_discover_tests registers each test separately and CTest launches a new process for each, which resets every static.
That last point is the one I most want to stress, because it inverts the usual intuition. Running under CTest looks like the more thorough setup, and it is the one that hides order-dependent tests. I have seen suites stay green in CI for a long time and then fail as soon as someone runs the binary locally to debug an unrelated test, because a singleton configured by one test leaks into the next. What I do now is run the binary directly in CI as well, with --gtest_shuffle, which randomizes the order and prints the seed (Note: Randomizing tests' orders with a seed of 45834 .). When I ran the example above with shuffle, it was First that failed instead of Second. Reproduce a failure with --gtest_random_seed=<seed>, and use --gtest_repeat=N to shake out flakiness.
The real fix is to design for isolation: inject dependencies instead of reaching for singletons, and give any unavoidable global a reset hook that the fixture’s SetUp calls.
Parameterized tests
When the same check applies to many inputs, TEST_P avoids copy-pasted tests and reports each input as its own test case:
struct ParseCase { std::string input; int expected; };
class ParseTest : public ::testing::TestWithParam<ParseCase> {};
TEST_P(ParseTest, ParsesDecimal) {
const auto& c = GetParam();
EXPECT_EQ(std::stoi(c.input), c.expected) << "input=" << c.input;
}
INSTANTIATE_TEST_SUITE_P(Decimal, ParseTest,
::testing::Values(ParseCase{"0", 0}, ParseCase{"42", 42},
ParseCase{"-7", -7}, ParseCase{" 5", 5}),
[](const ::testing::TestParamInfo<ParseCase>& info) {
return "case" + std::to_string(info.index);
});
This produces Decimal/ParseTest.ParsesDecimal/case0 through case3. Without the name generator the suffixes are just /0, /1, and so on, which is fine until a CI report says /17 failed and you have to count. A generator that returns something meaningful (it must be alphanumeric or underscore) pays for itself. Using a struct as the parameter keeps input and expected output together, which reads better than a std::pair or std::tuple.
Other generators: ::testing::ValuesIn(container), ::testing::Range(begin, end, step) (end excluded), ::testing::Bool(), and ::testing::Combine(...), which takes the Cartesian product. Combine(Values(1, 16, 256), Bool()) creates 6 tests. The product grows fast, so combine only dimensions that genuinely interact. If INSTANTIATE_TEST_SUITE_P is missing, recent GoogleTest versions report the uninstantiated TEST_P as a failure in GoogleTestVerification, instead of silently running zero tests.
Death tests
A death test checks that code terminates the process, for example by aborting on a violated precondition:
int checked_div(int a, int b) {
if (b == 0) {
std::fprintf(stderr, "checked_div: division by zero\n");
std::abort();
}
return a / b;
}
TEST(MathDeathTest, AbortsOnZero) {
EXPECT_DEATH(checked_div(1, 0), "division by zero"); // regex matched against stderr
}
TEST(MathDeathTest, ExitCode) {
EXPECT_EXIT(std::exit(3), ::testing::ExitedWithCode(3), "");
}
The caveats are where death tests go wrong:
- The statement runs in a child process. On Linux the default style forks; the
threadsafestyle (GTEST_FLAG_SET(death_test_style, "threadsafe")) re-executes the binary. On Windows the binary is always re-executed, which you can see in the output becauseRunning main() from gmock_main.ccis printed again for each death test. Side effects in the child, such as memory writes or mocks being called, never reach the parent. - Threads and fork do not mix. If the process already has threads when a fast-style death test forks, GoogleTest prints a warning about it, since only the forking thread survives in the child. Switch to the threadsafe style for suites that start threads.
assertdisappears in release builds. A death test built onassert(b != 0)fails with “it didn’t die” onceNDEBUGis defined. Either test an explicit check that stays in all builds (aschecked_divdoes), or useEXPECT_DEBUG_DEATH, which expects death only in debug builds.- The matcher is a regex on stderr, and on Windows GoogleTest uses its own simple regex syntax rather than POSIX extended regex. Keep the patterns to plain substrings.
- Name suites
*DeathTest. GoogleTest runs those suites first, before other tests have started threads. The run above showsMathDeathTestexecuting before every other suite, even though it is defined last in the file.
Mocking with gMock
gMock replaces a dependency with an object whose calls you can script and verify. The dependency needs a seam: usually a virtual interface, or a template parameter if you want to avoid virtual calls.
#include <gmock/gmock.h>
#include <gtest/gtest.h>
using ::testing::_;
using ::testing::InSequence;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::StrictMock;
class HttpClient {
public:
virtual ~HttpClient() = default;
virtual std::string get(const std::string& url) = 0;
virtual void log(const std::string& line) = 0;
};
class MockHttpClient : public HttpClient {
public:
MOCK_METHOD(std::string, get, (const std::string& url), (override));
MOCK_METHOD(void, log, (const std::string& line), (override));
};
class ApiService {
public:
explicit ApiService(HttpClient& c) : client_(c) {}
std::string user(const std::string& id) {
client_.log("fetch " + id);
return client_.get("https://api.example.com/users/" + id);
}
private:
HttpClient& client_;
};
TEST(ApiServiceTest, FetchesUser) {
MockHttpClient http;
EXPECT_CALL(http, get("https://api.example.com/users/7")).WillOnce(Return("{\"id\":7}"));
ApiService svc(http);
EXPECT_EQ(svc.user("7"), "{\"id\":7}");
}
MOCK_METHOD takes the return type, the name, the parameter list in parentheses and qualifiers such as (const, override). A return type containing a comma, such as std::map<int, int>, must also be wrapped in parentheses.
Uninteresting calls, NiceMock and StrictMock
FetchesUser passes, but user() also calls log(), which has no EXPECT_CALL. That is an uninteresting call, and a plain mock prints a warning without failing:
GMOCK WARNING:
Uninteresting mock function call - returning directly.
Function call: log(@0x1001ff5d0 "fetch 7")
NOTE: You can safely ignore the above warning unless this call should not happen.
The three flavors differ only in how they treat uninteresting calls:
| Mock type | Uninteresting call (no EXPECT_CALL for the method) |
|---|---|
MockHttpClient | GMOCK WARNING, test continues |
NiceMock<MockHttpClient> | silent |
StrictMock<MockHttpClient> | test fails with Uninteresting mock function call |
A call that matches no expectation on a method that does have EXPECT_CALLs is an unexpected call, and it fails under every flavor. The GoogleTest docs recommend NiceMock as the default, and I agree: StrictMock makes every internal refactor of the code under test break tests that were not about that call. The warning’s own advice is worth following too. Adding an EXPECT_CALL(http, log(_)) just to silence it turns an incidental detail into a requirement.
How gMock picks an expectation
Expectations are searched newest first. Declare the general case before the specific one:
TEST(ApiServiceTest, LaterExpectationWins) {
NiceMock<MockHttpClient> http;
EXPECT_CALL(http, get(_)).WillRepeatedly(Return("generic"));
EXPECT_CALL(http, get("https://api.example.com/users/1")).WillOnce(Return("specific"));
ApiService svc(http);
EXPECT_EQ(svc.user("1"), "specific");
EXPECT_EQ(svc.user("2"), "generic");
}
Reversing the two lines makes the catch-all shadow the specific one. There is a second surprise here: an expectation stays active after it has been satisfied. Calling svc.user("1") twice with the setup above does not fall through to the get(_) expectation. It over-saturates the WillOnce one:
Mock function called more times than expected - returning default value.
Function call: get(@0x1001ff550 "https://api.example.com/users/1")
Returns: ""
Expected: to be called once
Actual: called twice - over-saturated and active
Add .RetiresOnSaturation() to the specific expectation if later calls should fall through to the general one.
Calls are unordered by default. When order matters, put the expectations in an InSequence scope:
{
InSequence seq;
EXPECT_CALL(http, log("fetch 1"));
EXPECT_CALL(http, get(_)).WillOnce(Return("ok"));
}
Finally, set expectations before exercising the code. An EXPECT_CALL written after the call happened does not match it retroactively. It fails when the mock is destroyed at the end of the test: Actual function call count doesn't match EXPECT_CALL(http, get(_))... Expected: to be called once, Actual: never called - unsatisfied and active. gMock’s documentation calls interleaving EXPECT_CALLs with calls to the mock undefined behavior, so keep all expectations in the arrange step.
My own rule of thumb for mocks is to mock the boundaries (network, clock, filesystem, randomness) and use real objects for everything else. The common failure mode of heavy mocking is a test that restates the implementation call by call: it passes while the real integration is broken, and it fails every time someone reorders two harmless calls. If a test needs more than a few EXPECT_CALLs to exercise one behavior, that usually tells me the unit under test has too many collaborators.
Useful command-line flags
./unit_tests --gtest_filter='SplitTest.*' # one suite
./unit_tests --gtest_filter='-*Slow*' # exclude by pattern
./unit_tests --gtest_list_tests # what gtest_discover_tests sees
./unit_tests --gtest_shuffle --gtest_repeat=20 # expose order dependence and flakiness
./unit_tests --gtest_break_on_failure # trap into the debugger at the failure
./unit_tests --gtest_output=xml:report.xml # JUnit-style report for CI
Inside loops, SCOPED_TRACE("i=" + std::to_string(i)); adds the iteration to any failure message raised in that scope, which beats printing debugging output by hand. For code that must reject malformed input, unit tests cover the cases you thought of; fuzz testing finds the ones you did not.
Related Articles
- C++ package managers: vcpkg and Conan
- CMake targets and usage requirements
- Advanced CMake
- Fuzz testing C++ code
- Virtual functions and interfaces
References
- GoogleTest documentation: https://google.github.io/googletest/ (Primer, Advanced topics, gMock for Dummies, gMock Cookbook)
- CMake
GoogleTestmodule:gtest_discover_testsin the CMake documentation