Skip to content

Latest commit

 

History

History
282 lines (230 loc) · 6.34 KB

cookbook.md

File metadata and controls

282 lines (230 loc) · 6.34 KB

Cookbook

The DrMock cookbook contains a collections of frequently required patterns.

Table of contents

Factories

Factories come into play when instances of a class Foo are dynamically created:

// IFoo.h
class IFoo
{
public:
  virtual ~IFoo() = default;

  virtual int func() = 0;
  /* ... */
};

// Foo.h
class Foo : public IFoo
{
public:
  Foo(int)
  {
    /* ... */
  }

  /* ... */
};

// Bar.h
class Bar
{
public:
  void gunc()
  {
    int x = /* ... */;
    auto foo = std::make_shared<Foo>(x);
    /* Do something with `foo`... */
  }
};

Suppose we want to mock the role of the Foo instances in the method Bar::gunc. The dependency injection cannot be done via templates. Let's try:

// Bar.h
template<typename T = Foo>
class Bar
{
public:
  void gunc()
  {
    int x = /* ... */;
    auto foo = std::make_shared<T>(x);
    /* Do something with `foo`... */
  }
};

In production code, we'd just use Bar, in test code, we could use Bar<FooMock> to produce mock objects. But FooMock doesn't have a ctor other than it's default, and there is not way to "reach" the produced mock objects for configuration.

Instead, the dependency injections should happen via the factory pattern:

// AbstractFactory.h
template<typename Base, typename... Args>
class AbstractFactory
{
public:
  virtual ~AbstractFactory() = default;

  virtual std::shared_ptr<Base> make_shared(Args...) = 0;
};

// Factory.h
template<typename Base, typename Derived, typename... Args>
class Factory final : public AbstractFactory<Base, Args...>
{
public:
  std::shared_ptr<Base> make_shared(Args... args) override
  {
    return std::make_shared<Derived>(std::move(args)...);
  }
};

// Bar.h
class Bar
{
public:
  Bar()
  :
    Bar{std::make_shared<Factory<IFoo, Foo, int>>()}
  {}

  Bar(std::shared_ptr<AbstractFactory<IFoo, int>> factory)
  :
    factory_{std::move(factory)}
  {}

  void gunc()
  {
    int x = /* ... */;
    auto foo = factory_->make_shared(x);
    /* Do something with `foo`... */
  }

private:
  std::shared_ptr<AbstractFactory<IFoo, int>> factory_{};
};

By default, instances of Bar behave just as in the example above (as a normal Factory is used by default), but in test code for the Bar class, a dependency injection may be achieved by mocking AbstractFactory and IFoo using DrMock and injecting a FactoryMock into the system under test via the ctor Bar(std::shared_ptr<AbstractFactory<IFoo, int>>):

DRTEST_TEST(factoryExample)
{
  auto factory = std::make_shared<FactoryMock<IFoo, int>>();

  // Create and configure output for `make_shared`:
  auto foo1 = std::make_shared<FooMock>();
  foo1->mock.func().returns(1).times(1);
  auto foo2 = std::make_shared<FooMock>();
  foo2->mock.func().returns(2).times(1);

  factory->mock.make_shared().state().transition("",       "state2",    1)
                                     .transition("state2", "exhausted", 2);
  factory->mock.make_shared().state().returns("",       foo1)
                                     .returns("state2", foo2);

  // Inject factory object into test object.
  Bar bar{factory};

  // Run the tests.
  bar.gunc();  // Factory produces `foo1` for internal use.
  bar.gunc();  // Factory produces `foo2` for internal use.
  /* ... */

  // Verify that `foo1` and `foo2` are used exactly once, as defined above.
  DRTEST_VERIFY_MOCK(foo1->mock.func());
  DRTEST_VERIFY_MOCK(foo2->mock.func());
}

Note in order to configure the produced instances of FooMock, it's necessary to mock the factory itself. Also note that the assertion that foo1 and foo2 are called exactly once implies the assertion that factory->make_shared() was called twice, with the arguments 1 and 2.

The QObject-Interface-Implementation pattern

Class templates may not be QObjects. If you want to define a templated QObject interface, you can't just do

template<typename T>
class IFoo : public QObject
{
  Q_OBJECT

public:
  virtual ~IFoo() = default;

  virtual T f() = 0;  // Note: f depends on the template parameter!

public slots:
  virtual void slot() = 0;

signals:
  /* ... */
};

template<typename T>
class Foo : public Foo<T>
(
  /* ... */
};

or you'll run into trouble. Instead, introduce an extra layer of abstraction:

// FooQObject.h
class FooQObject : public QObject
{
  Q_OBJECT

public:
  virtual ~FooQObject() = default;

public slots:
  virtual void slot() = 0;

signals:
  /* ... */
};

// IFoo.h
template<typename T>
class IFoo : public FooQObject
{
public:
  virtual T f() = 0;

  virtual void slot() = 0;  // Required so that DrMock finds this function.
};

// Foo.h/Foo.cpp
template<typename T>
class Foo : public Foo<T>
{
public:
  T f() override
  {
    /* ... */
  }
  void slot() override
  {
    /* ... */
  }

  /* ... */
};

All slots and signals go into FooQObject. Slots are pure virtual, signals are defined by the MOC. It's prudent not to use QFoo instead of FooQObject to prevent collisions with the Qt namespace.

The other pure virtual methods, even those whose signature doesn't contain any template parameter, are declared in IFoo, alongside duplicates of all public slots (note the occurence of slot in IFoo). This is required so that all pure virtual methods are declared in the interface that DrMock receives. If you fail to do so, DrMock will not implement the slots, FooMock will be abstract instead of an implementation of IFoo, and the test code will most likely throw a compilation error similar to this one:

error: abstract class is marked 'final' [-Werror,-Wabstract-final-class] class FooMock final : public IFoo<T>