GTest/GMock Testing Skill
Use this skill when writing unit tests for C++ code using Google Test (GTest) and Google Mock (GMock).
"Implementation is complete. Would you like me to add unit tests?
- Which components/functions should be tested?
- Are there specific edge cases to cover?"
If user agrees, follow the planning process below.
Test-by-test approval: Write ONE test at a time, then STOP and wait for user approval before writing the next test.
Phase 1: Analyze Testable Code
Before writing tests, identify:
- Public API - Functions/methods that should be tested
- Dependencies - What needs to be mocked
- Edge cases - Boundary conditions, error states, empty inputs
- Invariants - Conditions that must always hold
Phase 2: Create Test Plan
Save to planning/tests-<component>.md:
# Test Plan: <Component Name>
## Scope
<What is being tested>
## Test Cases
### <Function/Class 1>
- [ ] Test: <test name> - <what it verifies>
- [ ] Test: <test name> - <what it verifies>
#### Edge Cases
- [ ] <edge case description>
- [ ] <edge case description>
### <Function/Class 2>
...
## Mocks Required
- [ ] `mock_<name>` - mocks `<interface>` for <purpose>
- [ ] `mock_<name>` - mocks `<interface>` for <purpose>
## Test Fixtures
- [ ] `<fixture_name>` - shared setup for <test group>
Phase 3: Implement Tests (One by One)
- Write ONE test
- Show it to the user
- Say: "Here's the test for . Is it OK? Should I continue with the next test?"
- WAIT for user approval
- Only after approval, proceed to next test
- Mark completed test in plan file
DO NOT write multiple tests at once!
GTest/GMock Patterns
Basic Test Structure
#include <gtest/gtest.h>
#include <gmock/gmock.h>
// Test case naming: TestSuiteName_TestName
TEST(calculator_test, add_returns_sum_of_two_numbers)
{
calculator calc;
EXPECT_EQ(calc.add(2, 3), 5);
}
Test Fixture
class parser_test : public ::testing::Test
{
protected:
void SetUp() override
{
m_parser = std::make_unique<parser>();
}
void TearDown() override
{
m_parser.reset();
}
std::unique_ptr<parser> m_parser;
};
TEST_F(parser_test, parse_empty_input_returns_nullopt)
{
EXPECT_EQ(m_parser->parse(""), std::nullopt);
}
Creating Mocks
// Interface to mock
class i_database
{
public:
virtual ~i_database() = default;
virtual bool save(const record& r) = 0;
virtual std::optional<record> find(int id) = 0;
};
// Mock class
class mock_database : public i_database
{
public:
MOCK_METHOD(bool, save, (const record& r), (override));
MOCK_METHOD(std::optional<record>, find, (int id), (override));
};
Using Mocks
using ::testing::Return;
using ::testing::_;
using ::testing::NiceMock;
using ::testing::StrictMock;
TEST_F(service_test, process_saves_to_database)
{
NiceMock<mock_database> mock_db;
service svc(mock_db);
EXPECT_CALL(mock_db, save(_))
.WillOnce(Return(true));
EXPECT_TRUE(svc.process(some_data));
}
Common Matchers
using namespace ::testing;
// Value matchers
EXPECT_THAT(value, Eq(expected));
EXPECT_THAT(value, Ne(unexpected));
EXPECT_THAT(value, Lt(max));
EXPECT_THAT(value, Gt(min));
EXPECT_THAT(value, Le(max));
EXPECT_THAT(value, Ge(min));
// String matchers
EXPECT_THAT(str, StartsWith("prefix"));
EXPECT_THAT(str, EndsWith("suffix"));
EXPECT_THAT(str, HasSubstr("middle"));
EXPECT_THAT(str, MatchesRegex("pattern"));
// Container matchers
EXPECT_THAT(vec, IsEmpty());
EXPECT_THAT(vec, SizeIs(5));
EXPECT_THAT(vec, Contains(element));
EXPECT_THAT(vec, ElementsAre(1, 2, 3));
EXPECT_THAT(vec, UnorderedElementsAre(3, 1, 2));
EXPECT_THAT(vec, Each(Gt(0)));
// Pointer matchers
EXPECT_THAT(ptr, IsNull());
EXPECT_THAT(ptr, NotNull());
EXPECT_THAT(ptr, Pointee(Eq(value)));
// Optional matchers (custom or use value)
EXPECT_TRUE(opt.has_value());
EXPECT_EQ(opt.value(), expected);
Testing Exceptions
TEST(parser_test, parse_invalid_throws_exception)
{
parser p;
EXPECT_THROW(p.parse("invalid"), parse_error);
}
TEST(parser_test, parse_invalid_throws_with_message)
{
parser p;
EXPECT_THROW(
{
try {
p.parse("invalid");
} catch (const parse_error& e) {
EXPECT_THAT(e.what(), HasSubstr("invalid"));
throw;
}
},
parse_error
);
}
Parameterized Tests
class add_test : public ::testing::TestWithParam<std::tuple<int, int, int>>
{
};
TEST_P(add_test, returns_correct_sum)
{
auto [a, b, expected] = GetParam();
calculator calc;
EXPECT_EQ(calc.add(a, b), expected);
}
INSTANTIATE_TEST_SUITE_P(
calculator_tests,
add_test,
::testing::Values(
std::make_tuple(0, 0, 0),
std::make_tuple(1, 1, 2),
std::make_tuple(-1, 1, 0),
std::make_tuple(INT_MAX, 0, INT_MAX)
)
);
Edge Cases Checklist
Always consider testing:
Boundary Values
- Zero / empty
- One element / single character
- Maximum values (INT_MAX, SIZE_MAX)
- Minimum values (INT_MIN, 0 for unsigned)
- Just below/above boundaries
Error Conditions
- Null pointers (if applicable)
- Empty containers
- Invalid input
- Resource exhaustion
- Timeout scenarios
State Transitions
- Initial state
- After single operation
- After multiple operations
- After error recovery
Test Naming Convention
Use descriptive names that explain:
- What is being tested
- Under what conditions
- Expected result
// Pattern: <unit>_<scenario>_<expected_result>
TEST(parser_test, parse_empty_string_returns_nullopt)
TEST(parser_test, parse_valid_json_returns_document)
TEST(parser_test, parse_invalid_json_throws_parse_error)
TEST(calculator_test, divide_by_zero_throws_domain_error)
Test File Organization
tests/
├── CMakeLists.txt
├── unit/
│ ├── test_calculator.cpp
│ ├── test_parser.cpp
│ └── mocks/
│ ├── mock_database.hpp
│ └── mock_network.hpp
└── integration/
└── test_system.cpp
CMakeLists.txt for Tests
enable_testing()
find_package(GTest REQUIRED)
add_executable(unit_tests
unit/test_calculator.cpp
unit/test_parser.cpp
)
target_link_libraries(unit_tests
PRIVATE
GTest::gtest_main
GTest::gmock
mylib # Library being tested
)
include(GoogleTest)
gtest_discover_tests(unit_tests)