Some time ago, I was involved in refactoring old C++ code written in a PoC (Proof of Concept) format that was not optimized for testing.
C++ has always been interesting compared to Java and C# due to:
- Performance
- The ability to produce code that can’t be reverse-engineered
(excluding advanced disassembly tools like IDA Disassembler with X-ray plugins). For Java and C#, reverse-engineering is significantly easier. - OS API support (e.g., for Windows)
While you can use the Windows API in languages like Go, Ruby, Python, and C# with minimal effort, the API was primarily developed for C/C++ developers.
Everything works well with unit tests when you follow SOLID, TDD, DI, etc.
Of course, if your legacy code doesn’t contain any tests (unit tests, integration tests, system tests, e2e tests, etc.), a good starting point is to write tests before starting the refactoring process. This approach can save time and help prevent burnout.
In this article, I will outline Extract and Override technique which could be helpful for writing tests for legacy C++ code.
This technique is used when a part of the code makes writing test code difficult, such as when objects are hardcoded and created within the constructor or method.
Let’s say you have the following code
#include <iostream>
#include <string>
#include <stdexcept>
class DataProcessor {
public:
void processData() {
std::string data = fetchDataFromAPI(); // Hard-to-test dependency
std::cout << "Processing data: " << data << std::endl;
}
protected:
std::string fetchDataFromAPI() {
// Simulating a call to an external API
throw std::runtime_error("API call failed!"); // External dependency
}
};
As you can see, the public processData
method internally calls the private fetchDataFromAPI
method, which prevents us from writing unit tests for the processData
method.
The idea of this technique is to:
- Make the
fetchDataFromAPI
method protected and virtual. - Create a derived subclass of
DataProcessor
and overridefetchDataFromAPI
with our implementation that, for example, returns mock data. - Use the derived subclass in our tests.
So, the code would look like this
#include <gtest/gtest.h>
class MockDataProcessor : public DataProcessor {
protected:
std::string fetchDataFromAPI() override {
return "Mock API data"; // Return mock data
}
};
write unit test
TEST(DataProcessorTest, ProcessData_UsesMockAPI) {
MockDataProcessor mockProcessor;
// Capture output for validation
testing::internal::CaptureStdout();
mockProcessor.processData();
std::string output = testing::internal::GetCapturedStdout();
// Verify that the mocked API data is processed
EXPECT_EQ(output, "Processing data: Mock API data\n");
}
This technique is mentioned in Working Effectively with Legacy Code by Michael C. Feathers and in the book The Art of Unit Testing by Roy Osherove, so refer to these books for additional details.
It should also be noted that this technique typically requires fewer code changes compared to refactoring the code, where we would:
- Create a separate interface and class that handles fetching data from the external API.
- Inject the class into the constructor of the
DataProcessor
.