В контексте модульного теста C ++ должны ли абстрактный базовый класс иметь другие абстрактные базовые классы в качестве параметров функции? - PullRequest
0 голосов
/ 03 сентября 2018

Я пытаюсь реализовать универсальные тесты для нашей унаследованной базы кода C ++. Я прочитал Майкла Фезерса «Эффективная работа с унаследованным кодом» и получил некоторое представление о том, как достичь своей цели. Я использую GooleTest / GooleMock в качестве фреймворка и уже реализовал несколько первых тестов, включающих фиктивные объекты.

Для этого я попробовал подход «Извлечь интерфейс», который хорошо работал в одном случае:

class MyClass
{
  ...
  void MyFunction(std::shared_ptr<MyOtherClass> parameter);
}

стали:

class MyClass
{
  ...
  void MyFunction(std::shared_ptr<IMyOtherClass> parameter);
}

и я прошел ProdMyOtherClass в производстве и MockMyOtherClass в тесте. Пока все хорошо.

Но теперь у меня есть другой класс, использующий MyClass, например:

class WorkOnMyClass
{
  ...
  void DoSomeWork(std::shared_ptr<MyClass> parameter);
}

Если я хочу проверить WorkOnMyClass, и я хочу смоделировать MyClass во время этого теста, я должен снова извлечь интерфейс. И это приводит к моему вопросу, на который я до сих пор не нашел ответа: как бы выглядел интерфейс? Я предполагаю, что все должно быть абстрактно, поэтому:

class IMyClass
{
  ...
  virtual void MyFunction(std::shared_ptr<IMyOtherClass> parameter) = 0;
}

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

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

Ответы [ 2 ]

0 голосов
/ 03 сентября 2018

TLDR, выделенный жирным шрифтом

Как уже отмечал Джеффри Коффин, нет единственно правильного способа сделать то, что вы стремитесь достичь. В программном обеспечении не существует «одного размера для всех», поэтому ответьте на все эти ответы с небольшим количеством соли и используйте ваше лучшее решение для вашего проекта и обстоятельств. Как говорится, вот одна потенциальная альтернатива:

Остерегайтесь насмешливого ада:

Подход, который вы обрисовали в общих чертах, будет работать: но он может быть не лучшим (или может быть, только вы можете решить). Обычно причина, по которой вы склонны использовать макеты, заключается в том, что есть некоторая зависимость, которую вы хотите сломать. Извлечение интерфейса - это нормальный шаблон, но он, вероятно, не решает основную проблему. В прошлом я сильно полагался на насмешки, и были ситуации, когда я действительно сожалел об этом. У них есть свое место, но я стараюсь использовать их как можно реже и с наименьшим уровнем и наименьшим возможным классом. Вы можете попасть в насмешливый ад, в который вы собираетесь войти, так как вы должны рассуждать о том, что ваши издевательства имеют насмешки. Обычно, когда это происходит, это происходит потому, что существует структура наследования / композиции, а base / children разделяют зависимость. Если возможно, вы хотите провести рефакторинг, чтобы зависимость не была так сильно укоренилась в ваших классах.

Выделение "реальной" зависимости:

Лучшим шаблоном может быть Parameterize Constructor (еще один паттерн Michael Feathers WEWLC).

WLOG, допустим, что ваша мошенническая зависимость - это база данных (возможно, это не база данных, но идея все еще верна). Возможно, MyClass и MyOtherClass оба нуждаются в доступе к нему. Вместо извлечения интерфейса для обоих этих классов, попытается изолировать зависимость и передать ее конструкторам для каждого класса.

Пример:

class MyClass {
public:
    MyClass(...) : ..., db(new ProdDatabase()) {}; // Old constructor, but give it a "default" database now
    MyClass(..., Database* db) : ..., db(db) {}; // New constructor
    ...
private:
    Database* db; // Decide on semantics about owning a database object, maybe you want to have the destructor of this class handle it, or maybe not
    // MyOtherClass* moc; //Maybe, depends on what you're trying to do
};

и

class MyOtherClass {
public:
    // similar to above, but you might want to disallow this constructor if it's too risky to have two different dependency objects floating around.
    MyOtherClass(...) : ..., db(new ProdDatabase());
    MyOtherClass(..., Database* db) : ..., db(db);
private:
    Database* db; // Ownership?
};

И теперь, когда мы видим этот макет, это заставляет нас понять, что вы могли бы даже хотеть, чтобы MyOtherClass просто был членом из MyClass (зависит от того, что вы делаете и как они связаны). Это позволит избежать ошибок при создании экземпляра MyOtherClass и облегчит бремя владения зависимостями.

Другая альтернатива - сделать Database синглтоном, чтобы облегчить бремя владения. Это будет хорошо работать для Database, но в общем случае шаблон синглтона не подходит для всех зависимостей.

Плюсы:

  • Разрешает чистое (стандартное) внедрение зависимостей, а решает основную проблему выделения истинной зависимости.
  • Изоляция реальной зависимости делает так, что вы избегаете насмешливого ада и можете просто обойти зависимость.
  • Улучшенный дизайн, пригодный для будущего, высокая возможность повторного использования шаблона и, вероятно, менее сложный. Следующему классу, который нуждается в зависимости, не придется издеваться над собой, вместо этого они просто привязывают зависимость в качестве параметра.

Минусы:

  • Этот шаблон, вероятно, займет больше времени / усилий, чем Extract Interface. В устаревших системах иногда это не срабатывает. Я совершил все виды грехов, потому что нам нужно было удалить особенность ... вчера. Это нормально, так бывает. Просто будьте в курсе проблем с дизайном и технической задолженности, которую вы накапливаете ...
  • Это также более подвержено ошибкам.

Некоторые общие унаследованные советы, которые я использую (о чем WEWLC не говорит):

Не зацикливайтесь на том, чтобы избежать зависимости, если вам не нужно ее избегать . Это особенно верно при работе с устаревшими системами, в которых рефакторинг рискован в генеральный. Вместо этого вы можете сделать так, чтобы ваши тесты вызывали фактическую базу данных (или любую другую зависимость), но набор тестов мог подключаться к небольшой «тестовой» базе данных вместо базы данных «prod». Стоимость выдерживания небольшой тестовой БД обычно довольно мала. Стоимость сбойного продукта, потому что вы обманули насмешку или издевались не так, как обычно, намного выше. Это также сэкономит вам лот кодирования.

Избегайте издевательств (особенно сильных издевательств), где это возможно. Я становлюсь все более и более убежденным, когда становлюсь инженером-программистом, что издевательства - это запах мини-дизайна. Они быстрые и грязные, но обычно иллюстрируют большую проблему.

Представьте идеальный API и попытайтесь создать то, что вы видите. Вы не можете на самом деле создать идеальный API, но представьте, что вы можете мгновенно реорганизовать все и получить тот API, который вам нужен. желание. Это хорошая отправная точка для улучшения унаследованной системы и компромиссов / жертв с вашим текущим дизайном / реализацией.

HTH, удачи!

0 голосов
/ 03 сентября 2018

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

Тем не менее, я бы призвал хотя бы немного предостеречь от использования наследования в этом случае. Большинство таких книг / авторов довольно сильно ориентированы на Java, где наследование рассматривается как швейцарский армейский нож (или, возможно, Leatherman) техник, используемых для каждой задачи, где она может быть как-то близка к осмыслению, независимо от того, действительно правильный инструмент для работы или нет. В C ++ наследование, как правило, рассматривается гораздо более узко, используется только тогда, когда / если / там, где альтернативы почти нет (и альтернатива заключается в том, чтобы в любом случае вручную свернуть то, что по сути является наследованием).

Основной уникальной особенностью наследования является полиморфизм во время выполнения. Например, у нас есть коллекция (указателей) объектов, и объекты в коллекции не все одного типа (но все они связаны через наследование). Мы используем виртуальные функции для обеспечения общего интерфейса с объектами различных типов.

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

Предполагая, что это правильно, наследование почти наверняка является неподходящим инструментом для работы. Когда вы имеете дело со статическим полиморфизмом (т. Е. Поведение определяется во время компиляции), существуют более совершенные инструменты (хотя и Feather, и компания, очевидно, вынуждены игнорировать это просто потому, что Java не может их предоставить).

В этом случае довольно просто обрабатывать всю работу во время сборки, не загрязняя производственный код какой-либо дополнительной сложностью. Например, вы можете создать исходный каталог с подкаталогами mock и production. В каталоге mock у вас есть foo.cpp, bar.cpp и baz.cpp, которые реализуют фиктивные версии классов Foo, Bar и Baz соответственно. В производственном каталоге у вас есть производственные версии того же самого. Во время сборки вы сообщаете инструменту сборки, создавать ли рабочую или макетную версию, и он выбирает каталог, в который он получает исходный код, основываясь на этом.

полуотносительно в сторону

Я также отмечаю, что вы используете shared_ptr в качестве параметра. Это еще один огромный красный флаг. Я нахожу случаи использования shared_ptr исключительно редкими. В огромном большинстве случаев я видел, как он использовался, на самом деле это не то, что нужно было использовать shared_ptr предназначен для случаев совместного владения объектом, но большинство случаев использования ближе к «Я не удосужился выяснить, кому принадлежит этот объект». shared_ptr не такая уж большая проблема сама по себе, но обычно это симптом более крупных проблем.

...