2

I would like to collect feedback on possible solutions for creating testable classes/interfaces.

In a normal C++ development we can use references/pointers and forward declarations to achieve dynamic polymorphism (see "Solution with references" below).

However, certain types of embedded software have constraints that prohibit (or recommend against) use of dynamic memory, virtual calls, delete/free, etc so that it is no longer possible to use normal dynamic polymorphism. In this case, it looks like the only solution for achieving polymorphism is to use templates (see "Solution with templates" below). This solution seems to be a good one, except that the code now has to always be in the header files and this can later contribute to a slower compilation time.

I am wondering what other solutions exist that allow having testable classes/interfaces:

  • When I am on a real hardware, I use the real driver implementation
  • When I am testing, I use the mock driver classes
  • I can switch between the real and mock interfaces easily ("dependency injection" approach).

The focus of my question is on possible solutions for archieving the polymorphism, not about discussing whether embedded software must always have all of the constraints mentioned above. If I want to avoid using virtual, how can I still have testable classes / interfaces?


Solution with references

// Driver.h
class Driver {
public:
  virtual void doHardwareCall() = 0;
};

// DriverUser.h
class Driver;
class DriverUser {
  Driver &driver;

public:
  DriverUser(Driver &driver);

  void foo();
};

// DriverUser.cpp
DriverUser::DriverUser(Driver &driver) : driver(driver) {}
void DriverUser::foo() {
  driver.doHardwareCall();
}

// Test file
#include "DriverUser.h"

class MockDriver : public Driver {
  void doHardwareCall() override {
    std::cout << "MockDriver::doHardwareCall()" << std::endl;
  }
};

TEST(DriverUser, usingMockDriver) {
  MockDriver mockDriver;
  DriverUser driverUser(mockDriver);

  driverUser.foo(); // => outputs "MockDriver::doHardwareCall()"
}

Solution with templates

// Driver.h
class Driver {
public:
  void doHardwareCall();
};

// DriverUser.h
class Driver;

template <typename T>
class DriverUser {
  T &driver;
public:
  DriverUser(T &driver) : driver(driver) {}
  void foo() {
    driver.doHardwareCall();
  }
};

// Test file
#include "DriverUser.h"

class MockDriver : public Driver {
public:
  void doHardwareCall() {
    std::cout << "MockDriver::doHardwareCall()" << std::endl;
  }
};

TEST(DriverUser, usingMockDriver) {
  MockDriver mockDriver;
  DriverUser<MockDriver> driverUser(mockDriver);

  driverUser.foo();
};
  • 1
    See also my answer to a very related question, and read the references there. – Basile Starynkevitch Aug 24 at 6:22
  • Thanks for the link, the information there is very useful even though it is not directly related. – Stanislav Pankevich Aug 27 at 9:37
  • 1
    The preface part seems to be rather messed up. No dynamic memory? Alright. No virtual calls? This is rather strange constraint. Virtual calls are trivial to implement though. Achieve dynamic polymorphism? Why would you want that? You later explain that you will use either real implementation (for deployment) or mocked implementation (for testing) that is no need for dynamic polymorphism, virtual calls or dynamic memory allocation on sight. "this can later contribute to a slower compilation time" - this can also later contribute to much faster compilation time. – user7860670 Aug 29 at 21:35
  • Your interpretation of the 'embedded constraints' is wrong. Where did you get them from? The reason for 'no dynamic memory' is because an embedded device frequently runs for a very long time. If 'out of the box' dynamic memory is used then memory fragmentation becomes a real issue. HOWEVER, that doesn't mean you don't override the new operator and manage pre-allocated different sized allocation memory heaps yourself. Why no virtual methods? Kind of eliminates the value of inheritance. At that point, drop C++ and use C. There's no reason you can't use C++ like C++ even on an embedded system. – Dunk Sep 3 at 20:55
  • VTT, Dunk, thank you both for the comments. I have rewritten the preface part a little bit to de-generalize the use of "embedded constraints" in my question. The focus is very technical: explore the space of the possible coding solutions. It wasn't my intention to impose any of the constraints mentioned in my question on ALL embedded software. – Stanislav Pankevich Sep 4 at 15:06
1

However with the embedded constraints of no dynamic memory, no virtual calls, no delete, no free etc it looks like the only solution for achieving polymorphism is to use templates

Your solution with references does not require the use of dynamic allocation (new), but it does require the use of virtual functions. If virtual functions are also forbidden by your coding standards (that is not always the case even for embedded development), then templates are indeed the only way to get some form of polymorphism in your codebase.

In my experience, there are two major ways of creating tests for embedded C++ code

  1. Use interfaces and references, as in your "solution with references". For the code running on target, the relevant objects would be created statically and hooked up

    // in main.cpp
    HardwareDriver driver;
    DriverUser driverUser(driver);
    

    For the unit-tests themselves, you can probably get an exception from the ban on dynamic allocation. That would make it easier to ensure the different tests don't interfere with each other without having to repeat the same setup code over and over again.

  2. Use different .cpp files for the implementing code, depending on if the code is being built for target or unit-test. In this case, there would be one "driver.h" header file and the build system would either use driver_target.cpp or driver_mocks.cpp to get the appropriate implementation of the Driver class.
    This means that some code will never be unit-tested, but consider that that code is probably so specific for the target hardware that it most likely won't compile on the system used for unit-testing anyway.

  • Thanks for the answer! This approach is definitely an option. In our case, the situation is a bit more complex though: we have more than one version of the MockDriver so we would like to have a real polymorphism instead of the one controlled by a build system like this solution suggests (compiling in .cpp files based on the platform). This is why the solution with templates is still our best candidate so far. – Stanislav Pankevich Aug 27 at 9:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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