In a C++ unit test context, should an abstract base class have other abstract base classes as function parameters?
Asked Answered
F

2

6

I try to implement uni tests for our C++ legacy code base. I read through Michael Feathers "Working effectively with legacy code" and got some idea how to achieve my goal. I use GooleTest/GooleMock as a framework and already implemented some first tests involving mock objects.

To do that, I tried the "Extract interface" approach, which worked quite well in one case:

class MyClass
{
  ...
  void MyFunction(std::shared_ptr<MyOtherClass> parameter);
}

became:

class MyClass
{
  ...
  void MyFunction(std::shared_ptr<IMyOtherClass> parameter);
}

and I passed a ProdMyOtherClass in production and a MockMyOtherClass in test. All good so far.

But now, I have another class using MyClass like:

class WorkOnMyClass
{
  ...
  void DoSomeWork(std::shared_ptr<MyClass> parameter);
}

If I want to test WorkOnMyClass and I want to mock MyClass during that test, I have to extract interface again. And that leads to my question, which I couldn't find an answer to so far: how would the interface look like? My guess is, that it should be all abstract, so:

class IMyClass
{
  ...
  virtual void MyFunction(std::shared_ptr<IMyOtherClass> parameter) = 0;
}

That leaves me with three files for every class: all virtual base interface class, production implementation using all production parameters and mock implementation using all mock parameters. Is this the correct approach?

I only found simple examples, where function parameters are primitives, but not classes, which in turn need tests themselves (and may therefore require interfaces).

Fitzpatrick answered 3/9, 2018 at 15:30 Comment(0)
K
5

TLDR in bold

As Jeffery Coffin has already pointed out, there is no one right way to do what you're seeking to accomplish. There is no "one-size fits all" in software, so take all these answers with a grain of salt, and use your best judgement for your project and circumstances. That being said, here's one potential alternative:

Beware of mocking hell:

The approach you've outlined will work: but it might not be best (or it might be, only you can decide). Typically the reason you're tempted to use mocks is because there's some dependency you're looking to break. Extract Interface is an okay pattern, but it's probably not resolving the core issue. I've leaned heavily on mocks in the past and have had situations where I really regret it. They have their place, but I try to use them as infrequently as possible, and with the lowest-level and smallest possible class. You can get into mocking hell, which you're about to enter since you have to reason about your mocks having mocks. Usually when this happens its because there's a inheritance/composition structure and the base/children share a dependency. If possible, you want to refactor so that the dependency isn't so heavily ingrained in your classes.

Isolating the "real" dependency:

A better pattern might be Parameterize Constructor (another Michael Feathers WEWLC pattern).

WLOG, lets say your rogue dependency is a database (maybe it's not a database, but the idea still holds). Maybe MyClass and MyOtherClass both need access to it. Instead of Extracting Interface for both of these classes, try to isolate the dependency and pass it in to the constructors for each class.

Example:

class MyClass {
public:
    MyClass(...) : ..., db(new ProdDatabase()) {}; // Old constructor, but give it a "default" database now
    MyClass(..., Database* db) : ..., db(db) {}; // New constructor
    ...
private:
    Database* db; // Decide on semantics about owning a database object, maybe you want to have the destructor of this class handle it, or maybe not
    // MyOtherClass* moc; //Maybe, depends on what you're trying to do
};

and

class MyOtherClass {
public:
    // similar to above, but you might want to disallow this constructor if it's too risky to have two different dependency objects floating around.
    MyOtherClass(...) : ..., db(new ProdDatabase());
    MyOtherClass(..., Database* db) : ..., db(db);
private:
    Database* db; // Ownership?
};

And now that we see this layout, it makes us realize that you might even want MyOtherClass to simply be a member of MyClass (depends what you're doing and how they're related). This will avoid mistakes in instantiating MyOtherClass and ease the burden of the dependency ownership.

Another alternative is to make the Database a singleton to ease the burden of ownership. This will work well for a Database, but in general the singleton pattern won't hold for all dependencies.

Pros:

  • Allows for clean (standard) dependency injection, and it tackles the core issue of isolating the true dependency.
  • Isolating the real dependency makes it so that you avoid mocking hell and can just pass the dependency around.
  • Better future proofed design, high reusability of the pattern, and likely less complex. The next class that needs the dependency won't have to mock themselves, instead they just rope in the dependency as a parameter.

Cons:

  • This pattern will probably take more time/effort than Extract Interface. In legacy systems, sometimes this doesn't fly. I've committed all sorts of sins because we needed to move a feature out...yesterday. It's okay, it happens. Just be aware of the design gotchas and technical debt you accrue...
  • It's also a bit more error prone.

Some general legacy tips I use (the things WEWLC doesn't tell you):

Don't get hell-bent about avoiding a dependency if you don't need to avoid it. This is especially true when working with legacy systems where refactorings are risky in general. Instead, you can have your tests call an actual database (or whatever the dependency is), but have the test suite connect to a small "test" database instead of the "prod" database. The cost of standing up a small test db is usually quite small. The cost of crashing prod because you goofed up a mock or a mock fell out of sync with reality is typically a lot higher. This will also save you a lot of coding.

Avoid mocks (especially heavy mocking) where possible. I am becoming more and more convinced as I age as a software engineer that mocks are mini-design smells. They are the quick and dirty: but usually illustrate a larger problem.

Envision the ideal API, and try to build what you envision. You can't actually build the ideal API, but imagine you can refactor everything instantly and have the API you desire. This is a good starting point for improving a legacy system, and make tradeoffs/sacrifices with your current design/implementation as you go.

HTH, good luck!

Khelat answered 3/9, 2018 at 18:25 Comment(1)
Thank you Matt for your explanation. I guess I was trying to strictly follow the guidelines from WEWLC where they don't really fit to my problem. I think one of your last points ("Don't get hell-bent about avoiding a dependency...") is crucial in my case. In order to write a test case, I currently have to prepare a lot of objects, which go into the tested function. I was trying to get around that, but maybe for now, that is just the way to go. At least I get some tests done and can then refactor to make future tests easier.Fitzpatrick
H
4

The first point to keep in mind is that there probably is no one way that's right and the others wrong--any answer is a matter of opinion as much as fact (though the opinions can be informed by fact).

That said, I'd urge at least a little caution against the use of inheritance for this case. Most such books/authors are oriented pretty heavily toward Java, where inheritance is treated as the Swiss army knife (or perhaps Leatherman) of techniques, used for every task where it might sort of come close to making a little sense, regardless of whether its really the right tool for the job or not. In C++, inheritance tends to be viewed much more narrowly, used only when/if/where there's nearly no alternative (and the alternative is to hand-roll what's essentially inheritance on your own anyway).

The primary unique feature of inheritance is run-time polymorphism. For example, we have a collection of (pointers to) objects, and the objects in the collection aren't all the same type (but are all related via inheritance). We use virtual functions to provide a common interface to the objects of the various types.

At least as I read things, that's not the case here at all though. In a given build, you'll deal with either mock objects or production objects, but you'll always know at compile time whether the objects in use are mock or production--you won't ever have a collection of a mixture of mock objects and production objects, and need to determine at run time whether a particular object is mock or production.

Assuming that's correct, inheritance is almost certainly the wrong tool for the job. When you're dealing with static polymorphism (i.e., the behavior is determined at compile time) there are better tools (albeit, ones Feather and company apparentlyy feel obliged to ignore, simply because Java fails to provide them).

In this case, it's pretty trivial to handle all the work at build time, without polluting your production code with any extra complexity at all. For one example, you can create a source directory with mock and production subdirectories. In the mock directory you have foo.cpp, bar.cpp and baz.cpp that implement the mock versions of classes Foo, Bar and Baz respectively. In the production directory you have production versions of the same. At build time, you tell the build tool whether to build the production or mock version, and it chooses the directory where it gets the source code based on that.

Semi-unrelated aside

I also note that you're using a shared_ptr as a parameter. This is yet another huge red flag. I find uses for shared_ptr to be exceptionally rare. The vast majority of times I've seen it used, it wasn't really what should have been used. A shared_ptr is intended for cases of shared ownership of an object--but most use seems to be closer to "I haven't bothered to figure out the ownership of this object". The shared_ptr isn't all that huge of a problem in itself, but it's usually a symptom of larger problems.

Hatch answered 3/9, 2018 at 17:1 Comment(3)
Thank you Jerry for your detailed explanation! I have two questions: 1) How can I use the GoogleMock framework if I don't have an abstract base class? What I know is, that I could use, but that brings its own problems I guess. 2) The shared_ptr was just an example, we normally have pass by pointer or by reference and only very few occasions where we pass by shared_ptr. But I know what you mean when reading your note.Fitzpatrick
The OP stated this was a legacy project, so it might not be as easy as you think to add a "test" build and a "prod" build. In the legacy projects I've worked, the build systems were usually a complete mess. Also, I think having a "test" and "prod" build encourages subtle bugs, and makes it so that you can only reason about dependency injection at compile time. Using Extract Interface allows you to remain flexible in this regard (and flexibility is king in legacy, because the next explosion or insane new feature is always lurking around the corner). Nice answer tho, +1Khelat
@MattMessersmith: Oh, I fully realize it may be non-trivial. On the other hand, the approach he's taking isn't entirely trivial either. It may be that the build is so much worse than the source code that doing the job at build time is impractical--but at the very least, it should be considered and discarded only if it really is impractical. Hacking up the source code just to support testing should be (more or less) a last resort, not the first.Hatch

© 2022 - 2024 — McMap. All rights reserved.