Модульное тестирование: кодирование интерфейсов?

В настоящее время мой проект состоит из различных конкретных классов. Теперь, когда я вхожу в модульное тестирование, похоже, что я должен создать интерфейс для каждого класса (фактически удваивая количество классов в моем проекте)? Я использую Google Mock в качестве основы для насмешек. См. Google Mock CookBook по интерфейсам. Хотя раньше у меня могли быть только занятия Car а также Engine, теперь у меня были бы абстрактные классы (иначе интерфейсы C++) Car а также Engine а затем классы реализации CarImplementation а также EngineImpl или что угодно. Это позволило бы мне заглушить Carзависимость от Engine,

Есть две линии мысли, с которыми я столкнулся при исследовании этого:

  1. Используйте интерфейсы только тогда, когда вам может понадобиться более одной реализации данной абстракции и / или для использования в публичных API, так что иначе не создавайте интерфейсы без необходимости.

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

При модульном тестировании я должен создать интерфейс для каждого класса в моем проекте? (Я склоняюсь к созданию интерфейсов для удобства тестирования)

3 ответа

Думаю, у вас есть несколько вариантов. Как вы говорите, одним из вариантов является создание интерфейсов. Скажем, у вас есть классы

class Engine:
{
public:
    void start(){ };
};

class Car
{
public: 
    void start()
    {
        // do car specific stuff
        e_.start();

private:
    Engine e;
};

Чтобы ввести интерфейсы - вам придется сменить автомобиль, чтобы взять двигатель

class Car
{
public: 
    Car(Engine* engine) :
    e_(engine)
    {}

    void start()
    {
        // do car specific stuff
        e_->start();

private:
    Engine *e_;
};

Если у вас есть только один тип двигателя - вы внезапно усложнили использование объектов Car (кто создает двигатели, кто владеет двигателями). У автомобилей много запчастей - поэтому эта проблема будет продолжать расти.

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

class Car<type EngineType = Engine>
{
public: 
    void start()
    {
        // do car specific stuff
        e_.start();

private:
    EngineType e;
};

Затем вы можете создавать автомобили со специализированными двигателями:

Car<MockEngine> testEngine;

Другой, другой подход, заключается в добавлении методов в Engine для проверки его, что-то вроде:

class Engine:
{
public:
    void start();
    bool hasStarted() const;
};

Затем вы можете либо добавить метод проверки в Car, либо наследовать от Car для тестирования.

class TestCar : public Car
{
public:
    bool hasEngineStarted() { return e_.hasStarted(); }
};

Это потребует, чтобы Engine был изменен с частного на защищенный в классе Car.

В зависимости от реальной ситуации, будет зависеть, какое решение лучше. Кроме того, у каждого разработчика будет свой святой Грааль того, как, по их мнению, код должен тестироваться модулем. Мои личные взгляды - помнить клиента / клиента. Предположим, что ваши клиенты (возможно, другие разработчики в вашей команде) будут создавать автомобили и не заботятся о двигателях. Поэтому я не хотел бы раскрывать понятия Engines (внутренний класс для моей библиотеки) просто для того, чтобы я мог выполнить модульное тестирование. Я бы предпочел не создавать интерфейсы и тестировать два класса вместе (третий вариант, который я дал).

Есть две категории тестирования относительно видимости реализации: тестирование черного ящика и тестирование белого ящика

  • Тестирование черного ящика фокусируется на тестировании реализации через их интерфейсы и проверке соответствия их спецификации.

  • Тестирование белого ящика тестирует детальные подробности о реализации, которые НЕ ДОЛЖНЫ быть вообще доступны извне. Этот вид тестирования подтвердит, что компоненты реализации работают так, как задумано. Таким образом, их результаты в основном представляют интерес для разработчиков, пытающихся выяснить, что сломано, или нуждается в обслуживании

mock по определению вписывается в модульную архитектуру, но из этого не следует, что все классы в проекте должны быть полностью модульными. Прекрасно провести какую-то линию, когда группа классов узнает друг о друге. Они как группа могут представлять другим модулям с точки зрения некоторого класса интерфейса фасада. Тем не менее, вы все равно захотите иметь в этом модуле тестовые драйверы белого ящика со знаниями о деталях реализации. Следовательно, этот вид тестирования не подходит для макетов.

Из этого тривиально следует, что вам не нужно иметь макеты или интерфейсы для всего. Просто возьмите высокоуровневые компоненты дизайна, которые реализуют фасадные интерфейсы и создайте для них макеты. Это даст вам приятное место, где фиктивные испытания окупаются ИМХО

Сказав это, попробуйте использовать инструмент в соответствии с вашими потребностями, а не позволять инструменту заставлять вас вносить изменения, которые, по вашему мнению, не принесут пользы

Создание интерфейсов для каждого класса в вашем проекте может или не может быть необходимым. Это полностью дизайнерское решение. Я обнаружил, что в основном это не так. Часто в n-уровневом дизайне вы хотите абстрагировать слой между доступом к данным и логикой. Я бы сказал, что вы должны работать над этим, так как это помогает в тестировании логики без большой инфраструктуры, необходимой для тестов. Методы абстракции, такие как внедрение зависимостей и IoC, потребуют от вас сделать что-то подобное и упростят тестирование указанной логики.

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

Другие вопросы по тегам