Introduction: Why Testing is Critically Important for C++ Projects
Testing C++ code is not just a formality, but a fundamental practice that ensures the stability, security, and maintainability of software. Unlike interpreted languages, where an error can manifest instantly, in C++, bugs often remain hidden until compilation or execution under specific conditions. A missed memory leak, undefined behavior, or incorrect pointer handling can lead to fatal crashes.
The modern software development industry distinguishes three main levels of testing: Unit, Integration, and E2E (End-to-End). Each solves its own task, and together they form the "testing pyramid," allowing up to 90% of errors to be caught before production release.
In this article, we will break down each type of test with practical examples in C++, using the popular frameworks Google Test and Catch2, and also discuss best practices for their application.
1. Unit Testing: Checking Atomic Modules
Unit tests are the lowest and fastest level. They check individual functions, classes, or methods in isolation from the rest of the system. The goal is to ensure that every "building block" of code works correctly.
Unit Test Example with Google Test
Consider a simple mathematical function:
// math_utils.h#pragma once
namespace MathUtils { int add(int a, int b); int divide(int a, int b);}// math_utils.cpp#include "math_utils.h"
namespace MathUtils { 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; }}Now let's write unit tests using Google Test:
// test_math_utils.cpp#include <gtest/gtest.h>#include "math_utils.h"
TEST(MathUtilsTest, AddPositiveNumbers) { EXPECT_EQ(MathUtils::add(2, 3), 5); EXPECT_EQ(MathUtils::add(0, 0), 0);}
TEST(MathUtilsTest, AddNegativeNumbers) { EXPECT_EQ(MathUtils::add(-1, -1), -2); EXPECT_EQ(MathUtils::add(-5, 3), -2);}
TEST(MathUtilsTest, DivideNormal) { EXPECT_EQ(MathUtils::divide(10, 2), 5); EXPECT_EQ(MathUtils::divide(7, 3), 2); // Integer division}
TEST(MathUtilsTest, DivideByZeroThrows) { EXPECT_THROW(MathUtils::divide(5, 0), std::invalid_argument);}Using Mock Objects
To isolate a module, it is often necessary to substitute dependencies. Google Mock (part of Google Test) allows creating stubs:
// database_interface.hclass DatabaseInterface {public: virtual ~DatabaseInterface() = default; virtual bool save(const std::string& data) = 0;};
// user_service.cppclass UserService { DatabaseInterface& db;public: explicit UserService(DatabaseInterface& db) : db(db) {} bool registerUser(const std::string& name) { return db.save("user:" + name); }};
// test_user_service.cpp#include <gmock/gmock.h>
class MockDatabase : public DatabaseInterface {public: MOCK_METHOD(bool, save, (const std::string&), (override));};
TEST(UserServiceTest, RegisterUserCallsDb) { MockDatabase mockDb; EXPECT_CALL(mockDb, save("user:Alice")).WillOnce(testing::Return(true));
UserService service(mockDb); EXPECT_TRUE(service.registerUser("Alice"));}Pros: Execution speed (milliseconds), precise error localization, ability to run without external dependencies.
Cons: Do not check interaction between real components.
2. Integration Testing: Checking Component Connections
Integration tests check how multiple mo