Appendix B: Testing
How do you know your code works? You run it and check the output. But as your program grows, manually checking every feature after every change becomes impossible. Automated tests solve this: small programs that exercise your code and verify the results. A good test suite catches bugs before your users do and gives you confidence to refactor without fear. This appendix covers unit testing concepts, two popular frameworks (Google Test and Catch2), test-driven development, and mocking.
Unit Testing Concepts
A unit test tests one small piece of code — a function, a class method, or a small module — in isolation.
Arrange, Act, Assert
Most unit tests follow the Arrange-Act-Assert pattern:
- Arrange — set up the test data and dependencies.
- Act — call the function or method under test.
- Assert — check that the result matches what you expect.
// Arrange
std::vector<int> scores = {90, 85, 92, 88};
// Act
int sum = std::accumulate(scores.begin(), scores.end(), 0);
// Assert
assert(sum == 355);What Makes a Good Test
- Focused: tests one thing. If it fails, you know exactly what broke.
- Independent: does not depend on other tests or shared state.
- Fast: runs in milliseconds, not seconds.
- Deterministic: produces the same result every time.
Google Test
Google Test (also called gtest) is the most widely used C++ testing framework.
Setting Up
With CMake (Appendix A):
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)
add_executable(tests test_math.cpp)
target_link_libraries(tests PRIVATE gtest_main)gtest_main provides a main() function, so your test file only needs test cases.
Writing Tests
#include <gtest/gtest.h>
int add(int a, int b) { return a + b; }
TEST(AddTest, PositiveNumbers)
{
EXPECT_EQ(add(2, 3), 5);
}
TEST(AddTest, NegativeNumbers)
{
EXPECT_EQ(add(-1, -2), -3);
}
TEST(AddTest, Zero)
{
EXPECT_EQ(add(0, 0), 0);
EXPECT_EQ(add(5, 0), 5);
}TEST(TestSuite, TestName) defines a test case. The suite name groups related tests.
Assertions
| Macro | Checks | On failure |
|---|---|---|
EXPECT_EQ(a, b) | a == b | Continues |
EXPECT_NE(a, b) | a != b | Continues |
EXPECT_LT(a, b) | a < b | Continues |
EXPECT_GT(a, b) | a > b | Continues |
EXPECT_TRUE(cond) | cond is true | Continues |
EXPECT_FALSE(cond) | cond is false | Continues |
EXPECT_THROW(expr, type) | expr throws type | Continues |
ASSERT_EQ(a, b) | a == b | Stops test |
EXPECT_* macros report failures but continue the test. ASSERT_* macros abort the test immediately — use them when continuing would cause a crash (e.g., after a null check).
Fixtures
When multiple tests need the same setup, use a fixture:
class PlaylistTest : public ::testing::Test {
protected:
void SetUp() override
{
playlist.push_back("Hey Ya!");
playlist.push_back("Toxic");
playlist.push_back("Crazy");
}
std::vector<std::string> playlist;
};
TEST_F(PlaylistTest, HasThreeSongs)
{
EXPECT_EQ(playlist.size(), 3);
}
TEST_F(PlaylistTest, ContainsToxic)
{
auto it = std::find(playlist.begin(), playlist.end(), "Toxic");
EXPECT_NE(it, playlist.end());
}TEST_F uses the fixture class. SetUp() runs before each test; TearDown() (optional) runs after each test. Each test gets a fresh instance — tests do not share state.
Running Tests
./tests # run all tests
./tests --gtest_filter="AddTest.*" # run only AddTest suite
./tests --gtest_list_tests # list all testsCatch2
Catch2 is a header-friendly alternative to Google Test with a more concise syntax.
Setting Up
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.2
)
FetchContent_MakeAvailable(Catch2)
add_executable(tests test_math.cpp)
target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)Writing Tests
#include <catch2/catch_test_macros.hpp>
int add(int a, int b) { return a + b; }
TEST_CASE("add returns correct sums", "[add]")
{
REQUIRE(add(2, 3) == 5);
REQUIRE(add(-1, -2) == -3);
REQUIRE(add(0, 0) == 0);
}Sections
Catch2’s sections replace fixtures for many use cases. Each section runs with a fresh state:
TEST_CASE("Playlist operations", "[playlist]")
{
std::vector<std::string> playlist = {"Hey Ya!", "Toxic"};
SECTION("adding a song increases size")
{
playlist.push_back("Crazy");
REQUIRE(playlist.size() == 3);
}
SECTION("clearing removes all songs")
{
playlist.clear();
REQUIRE(playlist.empty());
}
}Each SECTION runs independently — the playlist vector is reset between sections.
Assertions
| Catch2 | Behavior |
|---|---|
REQUIRE(expr) | Fatal — stops test on failure |
CHECK(expr) | Non-fatal — reports but continues |
REQUIRE_THROWS_AS(expr, type) | Checks that expr throws type |
Google Test vs. Catch2
| Feature | Google Test | Catch2 |
|---|---|---|
| Syntax | TEST(), EXPECT_*, ASSERT_* | TEST_CASE, REQUIRE, CHECK |
| Fixtures | Class-based (TEST_F) | Section-based |
| Setup | Requires linking | Header-friendly |
| Maturity | Industry standard | Popular, modern |
| Mocking | Built-in (Google Mock) | Separate libraries |
Both are excellent choices. Google Test is more common in industry; Catch2 is often preferred for smaller projects.
Test-Driven Development
Test-Driven Development (TDD) flips the usual workflow: you write the test first, then write the code to make it pass.
The Red-Green-Refactor Cycle
- Red: Write a test for a feature that does not exist yet. Run it — it fails (red).
- Green: Write the simplest code that makes the test pass (green).
- Refactor: Clean up the code while keeping the tests green.
Repeat for each new feature or behavior.
Example
Step 1 — Red:
TEST(Factorial, BaseCase)
{
EXPECT_EQ(factorial(0), 1);
EXPECT_EQ(factorial(1), 1);
}This fails because factorial does not exist.
Step 2 — Green:
int factorial(int n)
{
if (n <= 1) return 1;
return n * factorial(n - 1);
}Tests pass.
Step 3 — Refactor: The code is already clean, so nothing to change. Add the next test:
TEST(Factorial, LargerValues)
{
EXPECT_EQ(factorial(5), 120);
EXPECT_EQ(factorial(10), 3628800);
}When TDD Helps
- Well-defined requirements with clear inputs and outputs.
- Bug fixes: write a test that reproduces the bug, then fix the code.
- Library code where the API is designed upfront.
When TDD is Less Useful
- Exploratory or prototype code where requirements are unclear.
- UI code or systems with heavy external dependencies.
- Performance-sensitive code where the algorithm may change drastically.
Tip: Even if you do not follow strict TDD, writing tests alongside your code (rather than after) catches bugs earlier and keeps your code testable.
Mocking
A mock is a fake object that replaces a real dependency in a test. Mocking lets you test a class in isolation without needing a database, network, or filesystem.
Google Mock Basics
Google Mock comes with Google Test. It uses macros to create mock classes from interfaces (abstract classes, Chapter 1):
#include <gmock/gmock.h>
#include <gtest/gtest.h>
// Interface
class Database {
public:
virtual ~Database() = default;
virtual std::string lookup(int id) = 0;
virtual void save(int id, const std::string& data) = 0;
};
// Mock
class MockDatabase : public Database {
public:
MOCK_METHOD(std::string, lookup, (int id), (override));
MOCK_METHOD(void, save, (int id, const std::string& data), (override));
};Using the Mock
// The class under test
class MusicService {
public:
MusicService(Database& db) : db_(db) {}
std::string get_track_name(int id)
{
return db_.lookup(id);
}
private:
Database& db_;
};
// The test
TEST(MusicServiceTest, ReturnsTrackName)
{
MockDatabase mock_db;
EXPECT_CALL(mock_db, lookup(1))
.WillOnce(::testing::Return("Bohemian Like You"));
MusicService service(mock_db);
EXPECT_EQ(service.get_track_name(1), "Bohemian Like You");
}EXPECT_CALL sets an expectation: when lookup(1) is called, return "Bohemian Like You". If the method is never called, or called with different arguments, the test fails.
Key Google Mock Features
| Macro/Function | Purpose |
|---|---|
MOCK_METHOD(return, name, (args), (qualifiers)) | Define a mock method |
EXPECT_CALL(mock, method(args)) | Set an expectation |
.WillOnce(Return(value)) | Return a value once |
.WillRepeatedly(Return(value)) | Return a value every time |
.Times(n) | Expect exactly n calls |
Tip: Mocking works best when your code depends on interfaces (abstract classes) rather than concrete classes. This is another reason to use polymorphism (Chapter 1) in your designs.
Key Points
- Unit tests test small pieces of code in isolation using Arrange-Act-Assert.
- Good tests are focused, independent, fast, and deterministic.
- Google Test:
TEST()for test cases,EXPECT_*/ASSERT_*for assertions,TEST_Ffor fixtures. - Catch2:
TEST_CASEfor test cases,REQUIRE/CHECKfor assertions,SECTIONfor shared setup. - TDD (Test-Driven Development) follows the red-green-refactor cycle: write the test first, then the code.
- Mocking replaces real dependencies with fake objects for isolated testing. Google Mock provides
MOCK_METHODandEXPECT_CALL. - Mocking works best with interfaces (abstract classes).
- Write tests alongside your code, not after. Test coverage gives you confidence to refactor.
Exercises
Think about it: Why should tests be independent of each other? What problems can arise when tests share state?
Write a test (using Google Test syntax) for a function
bool is_palindrome(const std::string& s)that checks if a string reads the same forwards and backwards. Include tests for “racecar” (true), “hello” (false), and “” (true).Think about it: What is the difference between
EXPECT_EQandASSERT_EQin Google Test? When would you choose one over the other?Where is the bug?
TEST_F(PlaylistTest, CanRemoveSong) { playlist.erase(playlist.begin()); ASSERT_EQ(playlist.size(), 2); EXPECT_EQ(playlist[0], "Toxic"); }(Hint: what if the
SetUpadds songs in a different order than expected?)Think about it: When is TDD most useful? When is it less useful? Give an example of each.
Write a Catch2 test for a function
int clamp(int value, int lo, int hi)that restricts a value to a range. Use sections to test: value below range, value in range, and value above range.Think about it: Why does mocking require interfaces (abstract classes)? What happens if you try to mock a class with no virtual functions?
What does this test check?
TEST(StringTest, EmptyString) { std::string s; EXPECT_TRUE(s.empty()); EXPECT_EQ(s.size(), 0); EXPECT_EQ(s, ""); }Think about it: The text says “write tests alongside your code, not after.” Why is writing tests after the code is finished less effective?
Write a mock (using Google Mock syntax) for this interface and a test that uses it:
class Logger { public: virtual ~Logger() = default; virtual void log(const std::string& message) = 0; virtual int message_count() const = 0; };Write a class
Appthat takes aLogger&and has arun()method that callslog("Starting"). Test thatrun()callslogexactly once with the message “Starting”.