1

Mild pickle. I have a project which has components that are difficult to test/mock. It might look something like this:

class Contenxt;

class Server : public SomeOtherClass
{
public:
    ServerPlugin(const Contex&) {
        a.registerCallback([this](int i){ handleData(i); });
    }

    void handleData(int i)
    {
        int x = b.someFunc(i);
    }

    int getX() const { return x; }

private:
    ClassA a;
    ClassB b;
    int x{0};
};

For ServerPlugin::getX() to be tested, we need some type of control over at least ClassA to trigger the asynchronous process.

Obviously this is a simplification but should illustrate the issue. Note that while my class can be structured in just about any way, the main caveat is that I'm locked into that constructor and can't get around that. My preferred method would be to have ClassA & ClassB be template parameters with a constructor that took a reference to each. My Unit test could use a test version of them w/ all functionality implemented. But, I can't do that.

So AFAIK, I have the following options.

  • Modify the test framework to allow Friend classes access to private members. (I've seen this done before but is obviously somewhat controversial. Only listing for completeness.)
  • ClassA & ClassB could be protected members, and inherit the ServerPlugin object
  • Expose ClassA & ClassB via a public accessor. (This seems pretty terrible.)
  • Make ClassA & ClassB public. (Even more terrible)

So the good news is that the testing framework will test my class end to end just fine, but to me I've left out at least a good integration test, if not a unit-test. Also relying on a rather ominous end to end test runner makes for some slow development, and potentially daunting tasks for pinpointing the location of a bug... not to mention less flexibility with multiple inputs.

So, I'm Wondering if there's something I've missed, or if anyone has an opinion either way.

6
  • "But, I can't do that" - why, precisely? You seem to be able to change other things in the source code, so what exactly are you allowed to change, what not, and why?
    – Doc Brown
    Sep 21, 2020 at 16:44
  • Something else (the much older code base) owns it and calls it (and everything like it) with said constructor. I guess I won't say 100% impossible but probably like 99% impossible. It is something to consider but at this point I feel like it would be a decent little chunk of time via meetings with a very high probability of disappointment. Sep 21, 2020 at 16:53
  • 1
    The idea is not to change Server to Server<T_A, T_B>, but to provide a template class GenericServer<T_A, T_B> and a typedef GenericServer<ClassA, ClassB> Server;, so the "older base" will not have to be changed . Another idea could be to utilize the preprocessor (which is a suggestion from Feathers' well known bool "Working effectively with Legacy code"). But I guess one has to know more of your original code to see how much effort this would mean in your codebase.
    – Doc Brown
    Sep 21, 2020 at 16:59
  • That GenericServer idea might work. I was thinking about something like that but couldn't quite piece it together. Worth exploring. I actually have access to that book -- I'll check into that. Don't happen to know the chapter do you? I peaked into it a bit. Looked like a lot of it's suggestions were based on inheritance. Also worth noting, I'm new to this code base as well so I am going to have to do some investigations either way. Sep 21, 2020 at 17:10
  • 1
    Why could you not simply provide another constructor. Even if it is protected. That way you could pass in whatever A and whatever B, its not like C++ classes can only have one constructor.
    – Kain0_0
    Sep 22, 2020 at 2:51

2 Answers 2

4

You don't have to unit-test everything. Relying on integration or E2E tests is fine, especially when the aspect you want to test is at the interface of two components. Here, you have the interface between your class, and the asynchronous process A.

Testing that A will trigger the correct method call is likely a waste of time: creating this test requires a lot of effort for fairly little value.

However, you can test that handleData() works, with only minimal changes to your design: you just have to separate the callback registration from the rest of the code. For example:

class HandleDataForServerPlugin {
public:
  void handleData(int i) {
    x = b.someFunc(i);
  }
  int getX() const { return x; }
private:
  ClassB b;
  int x{0};
};

class ServerPlugin: public SomeOtherClass, private HandleDataForServerPlugin {
public:
  ServerPlugin(const Context& ) {
    a.registerCallback([this](int i) { handleData(i); })
  }
  // re-export some methods
  using HandleDataForServerPlugin::getX;
private:
  ClassA a;
};

You can now test HandleDataForPlugin directly, without a dependency on the ClassA trigger. Whether the dependency from the server plugin to ClassB is a problem depends on context, here I'll assume that using this class is an internal implementation detail of the handleData() logic.

Separating a class into an easy-to-test layer and a layer that adapts it to some public API is a pattern I've used occasionally when directly testing through the public API would be too cumbersome. In C++, you can also do this without exposing the lower layer publicly, unless you're implementing a header-only library. Additionally, splitting your classes like this is a zero-cost abstraction in C++.

If this separation is not possible, consider whether your ClassA and ClassB dependencies could not be changed to use an interface, and injected. You can still leave the single-parameter constructor intact.

class ServerPlugin: public SomeOtherClass {
public:
  ServerPlugin(const Context& ctx)
  : ServerPlugin(ctx, std::make_unique<ClassA>(), std::make_unique<ClassB>()) {}

  ServerPlugin(const Context&,
               std::unique_ptr<InterfaceA> a,
               std::unique_ptr<InterfaceB> b)
  : a{a}, b{b} {
    a.registerCallback([this](int i) { handleData(i); });
  }

  void handleData(int i) {
    x = b.someFunc(i);
  }

  int getX() const { return x; }
private:
  std::unique_ptr<InterfaceA> a;
  std::unique_ptr<InterfaceB> b;
  int x{0};
};

Dependency injection (here, as constructor injection) is the typical way to solve this kind of issue in other OOP languages. C++ just makes this more difficult because virtual methods are not the default, so the ability to inject a mock implementation for ClassB has to be an upfront design goal.

You will not be able to sensibly test all code paths (the path through the single-parameter constructor). But that path is not very risky since you only select the default ClassA and ClassB types.

2
  • This is great. I had never thought about constructor delegation. I like the first example as well. I always try to remember that "everything in computer science can be solved w/ a level of indirection". Was just having a hell of a time figuring out where to indirect. This will give me some stuff to chew on. Thank you. Sep 21, 2020 at 19:33
  • 1
    This! In C++ people are often so fixated on template meta programming and compile time mechanics that they forget about also run time mechanics for achieving the same goals.
    – Doc Brown
    Sep 22, 2020 at 4:44
2

I agree with what someone said that you should not try to test communications interfaces, if you really need it, then use Functional testing.

Otherwise if you really need to unit test it, then you should decouple dependencies by using dependency injection techniques in the constructor (constructor injecion). You can use boost DI or a service locator pattern.

The important part is that you should be able to mock the dependencies you are not testing, and for that you can use a base class that is a pure virtual.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.