Имеет ли Mock Objects в С++ всегда требуемые виртуальные методы или шаблоны?

Предположим, что у меня есть классы

class Inner {
  public:
    void doSomething();
};

class Outer {
  public:
    Outer(Inner *inner);  // Dependency injection.

    void callInner();
};

Надлежащее модульное тестирование говорит, что у меня должны быть тесты для Inner. Затем у меня должны быть тесты для Outer, который использует не реальный Inner, а скорее MockInner, чтобы я выполнял модульные тесты по функциональности, добавленной только Outer вместо полного стека Outer/Inner.

Чтобы сделать это, Googletest, кажется, предлагает превратить Inner в чистый абстрактный класс (интерфейс) следующим образом:

// Introduced merely for the sake of unit-testing.
struct InnerInterface {
  void doSomething() = 0;
};

// Used in production.
class Inner : public InnerInterface {
  public:
    /* override */ void doSomething();
};

// Used in unit-tests.
class MockInner : public InnerInterface {
  public:
    /* override */ void doSomething();
};

class Outer {
  public:
    Outer(Inner *inner);  // Dependency injection.

    void callInner();
};

Итак, в производственном коде я использовал бы Outer(new Inner); в тесте Outer(new MockInner).

OK. Кажется красивым в теории, но когда я начал использовать эту идею во всем коде, я обнаружил, что создаю чистый абстрактный класс для каждого классного класса. Это очень много печатающих панелей, даже если вы можете игнорировать незначительную производительность во время выполнения, что может привести к ненужной виртуальной отправке.

Альтернативный подход заключается в использовании шаблонов, как в следующем:

class Inner {
  public:
    void doSomething();
};

class MockInner {
  public:
    void doSomething();
};

template<class I>
class Outer {
  public:
    Outer(I *inner);

    void callInner();
};

// In production, use
Outer<Inner> obj;

// In test, use
Outer<MockInner> test_obj;

Это позволяет избежать котельного покрытия и ненужной виртуальной отправки; но теперь вся моя кодовая база находится в больших файлах заголовков, что делает невозможным скрытие исходных реализаций (не говоря уже о том, что приходится расстраивать ошибки компиляции шаблона и длительное время сборки).

Являются ли эти два метода, виртуальные и шаблоны единственными способами для правильного модульного тестирования? Существуют ли лучшие способы проведения надлежащего модульного тестирования?

При правильном модульном тестировании я имею в виду каждый unit тест проверяет только функциональные возможности, введенные этим устройством, но не зависимые от него также.

Ответ 1

Я не думаю, что вы должны издеваться над каждой зависимостью вашего испытанного класса на практике. Если это сложно создать, использовать или ощутить, тогда да. Также, если это напрямую зависит от некоторых ненужных внешних ресурсов, таких как БД, сеть или файловая система.

Но если ни одна из них не является проблемой, IMO, это нормально просто использовать экземпляр из него напрямую. Поскольку вы уже тестировали его, вы можете быть уверены, что он работает так, как ожидалось, и не мешает тестированию на более высоком уровне.

Я лично предпочитаю тесты рабочего блока и простую, чистую, поддерживаемую конструкцию, придерживаясь какой-то идеальной настройки пуристов unit test.

каждый юнит-тест проверяет только функциональные возможности, введенные этим блоком, но не зависимые от него.

Использование функциональности и тестирование функциональности - это две разные вещи.

Ответ 2

Я также думаю, что использование экземпляра на Inner прямо в порядке. Моя проблема заключается в издевательстве внешних объектов, которые не являются частью моего кода (предоставляются через статические библиотеки или библиотеки DLL, иногда сторонние). Я склонен переписывать ложную библиотеку DLL или библиотеку с теми же именами классов, а затем по-разному ссылаясь на тест. Изменение файла заголовка внешней зависимости для добавления "виртуальных" кажется мне неприемлемым. У кого-нибудь есть лучшее решение?