CODE: C CPP gtest unittest

unit tests are your safety net. They let you refactor aggressively, optimize confidently, and catch “it works on my machine” bugs before they become production incidents.

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)

Create a CMakeLists.txt like this:

cmake
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

bash
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

hpp
#pragma once

int add(int a, int b);
int divide(int a, int b); // throws on b==0

src/math.cpp

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

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 test
  • ASSERT_* = record failure and stop the test immediately

Example:

cpp
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

cpp
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

cpp
EXPECT_TRUE(cond);
EXPECT_FALSE(cond);

Floating-point (important)

Don’t do EXPECT_EQ for floats:

cpp
EXPECT_NEAR(value, expected, 1e-6);

Strings:

cpp
EXPECT_STREQ(c_str1, c_str2);     // C strings
EXPECT_EQ(std::string("a"), s);   // std::string

Exceptions

cpp
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

cpp
#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

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.

cpp
#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

cpp
EXPECT_EQ(actual, expected) << "when input=" << input;

`SCOPED_TRACE` (super helpful in loops)

cpp
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)

cpp
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

cpp
#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

cpp
#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:

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_Throws
  • Parse_EmptyString_ReturnsError
  • Encoder_InvalidHeader_Rejects

Arrange / Act / Assert (AAA)

Keep tests readable:

cpp
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:

bash
./build/my_tests

List tests:

bash
./build/my_tests --gtest_list_tests

Run a subset:

bash
./build/my_tests --gtest_filter=Math.*
./build/my_tests --gtest_filter=*Divide*

Repeat until failure (useful for flaky tests):

bash
./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.

cpp
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

cpp
#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

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

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