Advanced C++ Testing: from unit tests to end-to-end
In modern C++ development, code quality is determined not only by its performance but also by its reliability. Advanced testing goes far beyond simple checks — it is an architectural approach that includes unit tests, integration tests, and end-to-end (e2e) tests. In this article, we will explore how to properly build a testing pyramid for C++ projects using Google Test, Google Mock, and custom frameworks.
1. Unit Testing: isolation and mocks
Unit tests verify individual modules (functions, classes) in isolation. At an advanced level, the key task is to learn how to properly mock dependencies. Let's look at an example using Google Mock.
Suppose we have a class DatabaseConnector that depends on an external API. We want to test the data processing logic without accessing the real database.
#include <gmock/gmock.h>#include <gtest/gtest.h>
class DatabaseInterface {public: virtual ~DatabaseInterface() = default; virtual std::string fetchData(int id) = 0;};
class MockDatabase : public DatabaseInterface {public: MOCK_METHOD(std::string, fetchData, (int id), (override));};
class DataProcessor { DatabaseInterface& db;public: explicit DataProcessor(DatabaseInterface& db) : db(db) {} std::string process(int id) { auto raw = db.fetchData(id); if (raw.empty()) return "EMPTY"; return "PROCESSED: " + raw; }};
TEST(DataProcessorTest, ProcessWithValidData) { MockDatabase mockDb; EXPECT_CALL(mockDb, fetchData(42)) .WillOnce(testing::Return("Hello")); DataProcessor processor(mockDb); EXPECT_EQ(processor.process(42), "PROCESSED: Hello");}Using MOCK_METHOD allows precise control over dependency behavior. It is important to remember strict mocks (via NiceMock or StrictMock) to catch unexpected calls.
2. Integration Testing: testing interactions
Integration tests verify how modules work together. In C++, this often involves testing interactions with the file system, network protocols, or databases. An advanced approach is to use test fixtures and parameterized tests.
Example: testing a FileManager class that reads and writes files. We use temporary directories.
class FileManagerTest : public ::testing::Test {protected: std::filesystem::path tempDir; void SetUp() override { tempDir = std::filesystem::temp_directory_path() / "test_files"; std::filesystem::create_directories(tempDir); } void TearDown() override { std::filesystem::remove_all(tempDir); }};
TEST_F(FileManagerTest, WriteAndReadFile) { FileManager fm(tempDir.string()); fm.write("test.txt", "data"); auto content = fm.read("test.txt"); EXPECT_EQ(content, "data");}For parameterized tests, we use TEST_P with INSTANTIATE_TEST_SUITE_P. This allows running the same test with different input data.
class MathTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {};TEST_P(MathTest, Add) { auto [a, b, expected] = GetParam(); EXPECT_EQ(add(a, b), expected);}INSTANTIATE_TEST_SUITE_P(AddTests, MathTest, ::testing::Values( std::make_tuple(1, 1, 2), std::make_tuple(-1, 1, 0), std::make_tuple(0, 0, 0) ));3. End-to-End Testing: full cycle verification
E2e tests simulate end-user behavior. For C++ applications, this may involve running the full binary, checking its output, or interacting via an API. Advanced level