01 · GoogleTest Advanced¶
Level 1 covered TEST, EXPECT_EQ and fixtures — enough to test a calculator.
Real suites hit walls that basic assertions don't solve: the same logic checked
against twenty inputs, one contract checked against five types, a failure message
that says false is not true and nothing else. This module covers the GoogleTest
features that keep a suite readable as it grows past a few hundred tests.
1. Fixture lifecycle — per-test vs per-suite¶
SetUp/TearDown run once per test. SetUpTestSuite/TearDownTestSuite
run once for the whole suite and are static. Use the per-suite hooks only for
expensive, genuinely read-only setup — a loaded reference table, a temp
directory. The moment tests mutate per-suite state, you have order-dependent
tests.
class DatabaseTest : public ::testing::Test {
protected:
static void SetUpTestSuite() { // once, before the first test
schema_ = new Schema("fixtures/schema.sql");
}
static void TearDownTestSuite() { // once, after the last test
delete schema_;
schema_ = nullptr;
}
void SetUp() override { conn_.open(*schema_); } // before every test
void TearDown() override { conn_.close(); } // after every test
static Schema* schema_;
Connection conn_;
};
Schema* DatabaseTest::schema_ = nullptr;
The per-suite trap
Anything a test writes into schema_ leaks into every later test in the
suite. If you cannot make the shared object const, it probably belongs in
SetUp() instead. Verify with --gtest_shuffle (section 7).
2. Parameterized tests — one body, many inputs¶
When several tests differ only in their input and expected output, collapse them
into a TEST_P. Define a parameter struct, derive from
TestWithParam<T>, and instantiate.
#include <gtest/gtest.h>
#include <stdexcept>
#include <string>
int clampi(int v, int lo, int hi) {
if (lo > hi) throw std::invalid_argument("lo>hi");
return v < lo ? lo : (v > hi ? hi : v);
}
struct ClampCase { int v, lo, hi, want; };
class ClampTest : public ::testing::TestWithParam<ClampCase> {};
TEST_P(ClampTest, Clamps) {
const ClampCase& c = GetParam();
EXPECT_EQ(clampi(c.v, c.lo, c.hi), c.want);
}
INSTANTIATE_TEST_SUITE_P(Boundaries, ClampTest, ::testing::Values(
ClampCase{ 5, 0, 10, 5}, // interior
ClampCase{ -1, 0, 10, 0}, // below low
ClampCase{ 11, 0, 10, 10}, // above high
ClampCase{ 0, 0, 10, 0}, // exactly low
ClampCase{ 10, 0, 10, 10} // exactly high
), [](const testing::TestParamInfo<ClampCase>& i) {
return "case" + std::to_string(i.index); // readable test names
});
Build and run (GoogleTest installed via Homebrew; adjust the prefix for your system):
clang++ -std=c++17 -I/opt/homebrew/opt/googletest/include \
clamp_test.cpp -L/opt/homebrew/opt/googletest/lib \
-lgtest -lgtest_main -o clamp_test
./clamp_test
[----------] 5 tests from Boundaries/ClampTest
[ RUN ] Boundaries/ClampTest.Clamps/case0
[ OK ] Boundaries/ClampTest.Clamps/case0 (0 ms)
[ RUN ] Boundaries/ClampTest.Clamps/case1
[ OK ] Boundaries/ClampTest.Clamps/case1 (0 ms)
[ RUN ] Boundaries/ClampTest.Clamps/case2
[ OK ] Boundaries/ClampTest.Clamps/case2 (0 ms)
[ RUN ] Boundaries/ClampTest.Clamps/case3
[ OK ] Boundaries/ClampTest.Clamps/case3 (0 ms)
[ RUN ] Boundaries/ClampTest.Clamps/case4
[ OK ] Boundaries/ClampTest.Clamps/case4 (0 ms)
[----------] 5 tests from Boundaries/ClampTest (0 ms total)
Each case is a separate test: one failure names case2, and the other four
still run. That is the whole point — a loop inside a single TEST would stop at
the first failure and report one result.
Name your cases
Without the name-generator lambda you get Clamps/0, Clamps/1, … . With a
struct field you can do better still: return i.param.label and put a
human-readable label in the case. Failure output is the product.
3. Typed tests — one contract, many types¶
Parameterized tests vary the value. Typed tests vary the type. Use them when several implementations must satisfy the same contract.
template <typename T>
class ContainerTest : public ::testing::Test {
protected:
T c_;
};
using ContainerTypes = ::testing::Types<std::vector<int>, std::vector<long>>;
TYPED_TEST_SUITE(ContainerTest, ContainerTypes);
TYPED_TEST(ContainerTest, StartsEmpty) {
EXPECT_TRUE(this->c_.empty()); // note: this-> is required
}
[----------] 1 test from ContainerTest/0, where TypeParam = std::vector<int, std::allocator<int>>
[ RUN ] ContainerTest/0.StartsEmpty
[ OK ] ContainerTest/0.StartsEmpty (0 ms)
[----------] 1 test from ContainerTest/1, where TypeParam = std::vector<long, std::allocator<long>>
[ RUN ] ContainerTest/1.StartsEmpty
[ OK ] ContainerTest/1.StartsEmpty (0 ms)
this-> is not optional
In a template, members of a dependent base class are not visible to
unqualified lookup. Writing c_.empty() fails to compile with a message
about c_ not being declared. Always this->c_.
4. Matchers — assertions that explain themselves¶
EXPECT_THAT(value, matcher) produces far better failure text than a boolean.
Matchers come from <gmock/gmock.h> but work in plain GoogleTest.
#include <gmock/gmock.h>
using ::testing::ElementsAre;
using ::testing::HasSubstr;
using ::testing::AllOf, ::testing::Gt, ::testing::Lt;
using ::testing::UnorderedElementsAre;
EXPECT_THAT(name, HasSubstr("ell"));
EXPECT_THAT(v, ElementsAre(1, 2, 3)); // order matters
EXPECT_THAT(tags, UnorderedElementsAre("a", "b")); // order does not
EXPECT_THAT(score, AllOf(Gt(0), Lt(100)));
Compare the two failure messages for a vector mismatch:
# EXPECT_TRUE(v == expected)
Value of: v == expected
Actual: false
Expected: true
# EXPECT_THAT(v, ElementsAre(1, 2, 3))
Value of: v
Expected: has 3 elements where element #0 is equal to 1, ...
Actual: { 1, 2, 4 }, whose element #2 doesn't match
The second one tells you what to fix without opening a debugger.
5. Death tests¶
A death test asserts that code terminates the process — an assert, an
abort(), a fatal log. The statement runs in a forked child.
TEST(ConfigDeathTest, RejectsNegativeCapacity) {
EXPECT_DEATH(make_pool(-1), "capacity must be positive");
}
Naming matters: put death tests in a suite ending in DeathTest so GoogleTest
runs them before other suites, which avoids forking a process that already
holds threads.
Death tests and threads
Forking a multi-threaded process is only conditionally safe. If your fixture
starts threads, GoogleTest will print a warning about the death test style.
Prefer testing the precondition check on a single-threaded path, or switch
the API to throw so you can use EXPECT_THROW instead.
For exceptions — the far more common case in C++ — use the throw family, which needs no forking:
TEST(Clamp, RejectsInvertedRange) {
EXPECT_THROW(clampi(1, 10, 0), std::invalid_argument);
EXPECT_NO_THROW(clampi(1, 0, 10));
}
6. Locating failures inside helpers¶
A failure reported inside a shared helper points at the helper's line, not the
call site. SCOPED_TRACE adds the context back.
void ExpectValidUser(const User& u) {
EXPECT_FALSE(u.name.empty());
EXPECT_GT(u.id, 0);
}
TEST(UserTest, AllFixturesValid) {
for (const auto& u : load_fixtures()) {
SCOPED_TRACE("user id=" + std::to_string(u.id));
ExpectValidUser(u);
}
}
Helpers that use ASSERT_* must return void and be invoked through
ASSERT_NO_FATAL_FAILURE(Helper()) — otherwise the ASSERT returns from the
helper and the test carries on believing everything passed.
7. Runner flags that catch real bugs¶
./clamp_test --gtest_filter='Boundaries/*' # run a subset
./clamp_test --gtest_shuffle --gtest_repeat=20 # hunt order dependence
./clamp_test --gtest_brief=1 # only failures
./clamp_test --gtest_output=xml:results.xml # CI-consumable report
./clamp_test --gtest_list_tests # enumerate without running
--gtest_shuffle --gtest_repeat=20 is the single most valuable command here. A
suite that passes in file order and fails when shuffled has shared state, and
that shared state will eventually produce a flaky failure in CI at the worst
possible time.
8. Choosing the right tool¶
| Situation | Use | Not |
|---|---|---|
| Same logic, many input/expected pairs | TEST_P + INSTANTIATE_TEST_SUITE_P |
a loop inside one TEST |
| Same contract, several types | TYPED_TEST_SUITE |
copy-pasted suites |
| Container contents | EXPECT_THAT(v, ElementsAre(...)) |
EXPECT_TRUE(v == e) |
| Order-insensitive contents | UnorderedElementsAre |
sorting then comparing |
| Function throws | EXPECT_THROW |
try/catch + FAIL() |
Function calls abort() |
EXPECT_DEATH (suite named *DeathTest) |
EXPECT_THROW |
| Expensive read-only setup | SetUpTestSuite |
a file-scope global |
| Assertion inside a helper | SCOPED_TRACE |
guessing from the line number |
| Fatal check inside a helper | ASSERT_NO_FATAL_FAILURE(Helper()) |
bare Helper() |
How It Actually Works: parameterized tests, death tests, and matchers¶
TEST_Pgenerates one class,INSTANTIATE_TEST_SUITE_Pstamps out many registrations from it.TEST_Pdefines a template-like test class deriving from::testing::TestWithParam<T>, withGetParam()reading a member the harness sets before each run.INSTANTIATE_TEST_SUITE_Pis what actually loops over your generator (Values(...),Range(...)) at static registration time and registers oneTestInfoper parameter value into the same registry from Level 1 Module 7 — which is why the test count in your output genuinely multiplies, and why each instantiation gets its own name suffix (/0,/1, ...) baked in at that registration step.EXPECT_DEATHworks by forking, because a crash can't be "caught" in the parent. It callsfork()(or on some platforms, a thread + exec), runs your statement in the child, and lets the child'sSIGSEGV/abort()kill that process while the parentwaitpid()s on it and checks the child's exit status and captured stderr against your regex. This is fundamentally different fromEXPECT_THROW— a real crash never runs any more C++ code in that process, so there is no exception object to catch; the death test literally sacrifices a child process per assertion, which is also why death tests are slow and why GoogleTest insists they live in suites named*DeathTest(so you can filter them out of routine fast runs).EXPECT_THAT/matchers are composable predicate objects, not new syntax.ElementsAre(1, 2, 3)constructs aMatcher<Container>object whoseMatchAndExplainmethod is called with your actual container; matchers likeUnorderedElementsAreinternally run a bipartite-matching algorithm to find some assignment of actual elements to expected matchers, which is why it reports a specific unmatched element on failure instead of just "not equal." This sameMatcher<T>machinery is exactly what GoogleMock (next module) reuses forEXPECT_CALL(mock, Method(matcher))argument matching.
Exercise¶
Take the Statistics class you built at the end of Level 1 Module 7 (or write a
small TextBuffer class if you'd rather start fresh) and rework its suite.
-
Convert to parameterized tests. Replace your hand-written
meantests with a singleTEST_Pdriven by at least eight cases, including the empty input, a single element, values that don't divide evenly, and negative values. Supply a name generator so the failure output readsMean/StatsTest.Computes/negativesrather thanComputes/3. -
Add a typed test. Make
count_abovea template over the element type, then write aTYPED_TEST_SUITEcoveringint,long, anddoublethat asserts the "strictly greater" boundary holds for every type. Confirm the runner reports one test per type. -
Replace three boolean assertions with matchers. Find three
EXPECT_TRUE/EXPECT_FALSEassertions and rewrite them withEXPECT_THAT. Deliberately break the code under test, capture both failure messages, and write two sentences on what the matcher told you that the boolean did not. -
Prove independence. Add a
SetUpTestSuitethat loads a shared fixture vector, then deliberately have one testpush_backonto it. Run--gtest_shuffle --gtest_repeat=20and record the failure. Fix it by moving the state intoSetUp(), and re-run to confirm 20 clean passes. -
Trace a helper. Write an
ExpectSaneStats()helper used by three tests, break one input so it fails, and observe that the report names the helper. AddSCOPED_TRACEand show the improved output.