Domanda

I am working in a legacy C++ codebase and I want to test some methods on a class, DependsOnUgly, that has a dependency which is not easily broken on a big class (Ugly) with lots of external dependencies on the filesystem, etc. I want to get at least some methods of DependsOnUgly under test, while modifying the existing code as little as possible. There is no way to create a seam by a factory method, method parameter or constructor parameter without lots of code modifications; Ugly is a concrete class being depended on directly without any sort of abstract base class and has a great number of methods, few or none of which are marked virtual, that fully mocking it would be very drudgerous. I have no mock framework available, but I want to get DependsOnUgly under test so I can make changes. How can I break the external dependencies of Ugly to unit test the methods on DependsOnUgly?

È stato utile?

Soluzione

Use what I call a Preprocessor Mock—a mock injected via a preprocessor seam.

I first posted this concept in this question on Programmers.SE, and by the answers to that I judged that this was not a well known pattern, so I thought I ought to share it. I find it hard to believe that no one has done something like this before, but because I couldn't find it documented I thought I would share it with the community.

Here are notional implementations of Ugly and NotAsUgly for the sake of example.

DependsOnUgly.hpp

#ifndef _DEPENDS_ON_UGLY_HPP_
#define _DEPENDS_ON_UGLY_HPP_
#include <string>
#include "Ugly.hpp"
class DependsOnUgly {
public:
    std::string getDescription() {
        return "Depends on " + Ugly().getName();
    }
};
#endif

Ugly.hpp

#ifndef _UGLY_HPP_
#define _UGLY_HPP_
struct Ugly {
    double a, b, ..., z;
    void extraneousFunction { ... }
    std::string getName() { return "Ugly"; }
};
#endif

There are two basic variations. The first is where only certain methods of Ugly are called by DependsOnUgly, and you already want to mock those methods. The second is

Technique 1: Replace all of behavior of Ugly used by DependsOnUgly

I call this technique a Preprocessor Partial Mock because the mock only implements the necessary parts of the interface of the class being mocked. Use include guards with the same name as the production class in the header file for the mock class to cause the production class to never be defined, but rather the mock. Be sure to include the mock before DependsOnUgly.hpp.

(Note that my examples of a test file are not self-validating; this is simply for the sake of simplicity and to be unit test framework agnostic. The focus is on the directives at the top of the file, not the actual test method itself.)

test.cpp

#include <iostream>
#include "NotAsUgly.hpp"
#include "DependsOnUgly.hpp"
int main() {
    std::cout << DependsOnUgly().getDescription() << std::endl;
}

NotAsUgly.hpp

#ifndef _UGLY_HPP_ // Same name as in Ugly.hpp---deliberately!
#define _UGLY_HPP_
struct Ugly { // Once again, duplicate name is deliberate
    std::string getName() { return "not as ugly"; } // All that DependsOnUgly depends on
};
#endif

Technique 2: Replace some of behavior of Ugly used by DependsOnUgly

I call this a Subclassed-in-Place Mock because in this case Ugly is subclassed and the necessary methods overridden while the others are still available for use—but the name of the subclass is still Ugly. A define directive is used to renamed Ugly to BaseUgly; then an undefine directive is used, and the mock Ugly subclasses BaseUgly. Note that this may require marking something in Ugly as virtual depending on the exact situation.

test.cpp

#include <iostream>
#define Ugly BaseUgly
#include "Ugly.hpp"
#undef Ugly
#include "NotAsUgly.hpp"
#include "DependsOnUgly.hpp"
int main() {
    std::cout << DependsOnUgly().getDescription() << std::endl;
}

NotAsUgly.hpp

#ifndef _UGLY_HPP_ // Same name as in Ugly.hpp---deliberately!
#define _UGLY_HPP_
struct Ugly: public BaseUgly { // Once again, duplicate name is deliberate
    std::string getName() { return "not as ugly"; }
};
#endif

Note that both of these methods are a little precarious and should be used with caution. They should be moved away from as more of the codebase is under test and replaced with more standard means of breaking dependencies if possible. Note that they could both potentially be rendered ineffective if the include directives of the legacy codebase are messy enough. However, I have used them both successfully for actual legacy systems, so I know they can work.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top