Refactor legacy C++ code for testing / Extract and Override technique

By | December 22, 2024

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 override fetchDataFromAPI 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.

Leave a Reply