Pull to refresh

Comments 14

Отличная статья, показывающая что и как надо рефакторить в старом проекте для внедрения UnitTest-ов.


А главное теперь есть хороший пример, чтобы пресечь возражения такого рода: "Ну тут чтобы протестировать надо пол-проекта переписать :("

Спасибо за положительный отзыв! Очень рад, что вы нашли статью для себя полезной. Действительно, и я тоже на это надеюсь, она способна развить желание писать надежный тестируемый код.

Первый пример рефакторинга для улучшения тестируемости кода нормальный, но остальные просто жуть.


  1. фабрика:


    DbMngFactory::SetManager( 
      std::make_unique<FakeDatabaseManager>( true ) );
    EntryAnalyzer ea;

    EntryAnalyzer ea;
    DbMngFactory::SetManager( 
      std::make_unique<FakeDatabaseManager>( true ) );

    В двух приведенных примерах анализатор находится в разных состояниях, что крайне не очевидно.


  2. перегрузка метода:
    Вы не вызываете реальные методы БД, но сам менеджер инициализируется. Если запустить параллельно несколько тестов, то может быть достигнут лимит количества подключений к БД.
    Таким образом тест корректной проверки упадет, если несколько разработчиков запустят тесты одновременно. Или не упадет.
Спасибо за комментарии к приведенным примерам. Насчет указанных Вами замечаний хотел бы сказать следующее.
1. Вы правы, что крайне желательно писать тесты, которые понятны для чтения и прозрачны для понимания.
Однако при написании тестируемого кода мы, по сути, создаем его для еще одного пользователя — наших тестов. В указанном вами примере всю ответственность за создание необходимых для выполнения программы объектов мы возлагаем на фабрику. Это позволяет разграничить нам зоны ответственности классов. Но в этом случае для написания теста нам следует привести тестируемые объекты в требуемое состояния, что и является целью выполнения этапа Arrange.
2. В зависимости от особенностей архитектуры тестируемого класса, описанная вами ситуация потенциально, конечно, может возникнуть. Но если вы знаете такие архитектурные особенности, то целиком в вашей власти разорвать в тестируемом классе зависимость от БД полностью. В противном случае, как вы справедливо заметили, написанный тест будет взаимодействовать с реальной БД, что не даст ему права являться хорошим юнит-тестом.
Статья в целом весьма приятная.

Но вот пример для mock какой-то странный. Если уж мы там выделили в тестируемом коде работу с веб-сервисом в отдельную функцию (LogError), то никакие IWebService нам уже не требуются, т.к. мы можем банально завести переменную lastError и писать в неё прямо в классе TestingEntryAnalyzer.

Понятно, что это всё демонстрационные примеры для статьи. Но они всё же в них должен быть логический смысл — если используем mock, то должна быть видна потребность в таком решение.
Спасибо за отклик на статью. Вы правы, можно сделать проще. Но при написании тестов и соответствующего тестируемого кода нельзя выделить один путь, который является единственно верным. Как правильно, на мой взгляд, в своей книге отметил Roy Osherove, написание юнит-тестов — это есть искусство. Следует развивать умение чувствовать в каком случае и как лучше поступить. Возвращаясь к статье, мне хотелось показать основные принципы работы с fake-объектами, поэтому и был приведен указанный вами тест.
Во-первых, непонятно почему используя GoogleTest, вы не используете GoogleMock, а изобретаете очередной велосипед. Причем новый под каждый новый тест.
А во-вторых, в синтаксисе GoogleTest правильней использовать конструкции EXPECT_XXX, а не ASSERT_XXX. Т.к. макросы ASSERT прерывают выполнение тестов и вы увидите только первый фейл из всех возможных и не факт что это будет самый «правильный» фейл с точки зрения анализа root cause.
Спасибо за комментарии. Вы правы, можно применять GoogleMock для автоматизированного создания Mock-объектов. Но, как было сказано ранее, к числу важнейших характеристик хорошего юнит-теста следует отнести его читабельность, а также легкость его дальнейшей модификации и поддержки. Возможно, с помощью GoogleMock Mock-объекты можно быстрее создавать, но в дальнейшем, по моему опыту, тесты, их использующие, сложнее читать и тем более поддерживать.
Таким образом, в примерах, представленных в статье, GoogleMock намеренно не использовался, чтобы не усложнять рассказ о принципах создания тестируемого кода.

Касательно применения EXPECT_XXX и ASSERT_XXX. Как было сказано в статье, одна из рекомендаций заключается в том, чтобы использовать в каждом тесте только один assert. В этом случае, если сработал assert, то можно однозначно определить root case — найти причину его непрохождения.
Что-то мне подсказывает что научиться один раз читать конструкции GoogleMock все-же проще и перспективней, чем каждый раз разбираться как-же в этом месте написан тот или иной mock или stub…

Насчет единственного ASSERT, тоже требование так себе… Предположим у нас есть функция которая возвращает два значения, можно конечно сделать что-нибудь типа
ASSERT_TRUE(val1==xxx && val2==yyy);

но мне кажется правильней все-же писать
EXPECT_EQ(val1, xxx);
EXPECT_EQ(val2, yyy);

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

Говоря же в статье об использовании в тесте единственного assert, я хотел отметить, что не стоит в одном тесте подвергать проверке несколько несвязанных друг с другом задач. В указанном вами примере, мне кажется, может быть оправдано использование приведенного вами подхода – поскольку, по сути, тестируется один результат, возвращаемый функцией.

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

Если же эти данные никак не связаны друг с другом, то, возможно, нам следует повнимательней приглядеться к этой функции — а не возложена ли на нее слишком большая ответственность? Не пытается ли она решить внутри себя несколько несвязанных друг с другом задач? Не нужно ли запланировать ее рефакторинг?

О чем нам говорит возникновение таких мыслей? А говорит это о том, что возникающие вопросы о том, как написать тест для того или иного модуля — это хорошо. Подвергание проекта тестированию способствует «выпрямлению» кода — переосмыслению его архитектуры с целью выделения, в частности, прозрачных контрактов.

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

Также спасибо вам за аргументированный комментарий.
Да. Забыл еще вот что спросить. Зачем делать два конструктора, один дефолтный и один с параметром, если вместо этого можно сделать один конструктор с дефолтным параметром. Таким образом мы избегаем дублирования кода.
В принципе, в приведенном примере так, конечно, сделать можно. Но в этом случае, при работе над реальным проектом в перспективе может возникнуть ряд сложностей, связанных с поддержкой тестов, а также читабельностью кода боевого класса.
Немножко поутрирую. Например, пусть у нас есть класс, обладающий внешними зависимостями в количестве 100 штук, а для создания боевой версии объекта используется конструктор с 5 параметрами, часть из которых также задается по умолчанию. В таком случае, человеку, который разбирается с нашим кодом, придется потратить определенное количество времени, чтобы понять, какие из 105 параметров нужны для боевой версии, а какие — для тестирования. А если сроки горят, то все совсем нехорошо складывается. Так вот, чтобы избежать потенциальной агрессии с его стороны, все-таки рекомендуется разграничивать предназначения конструкторов, что и было сделано в указанном примере)
юнит тесты зло без функциональных, вроде всё потестено, а как оно всё вместе работает неизвестно :)
Спасибо за комментарий. Конечно, юнит-тесты не являются серебряной пулей, способной излечить все проблемы проекта. Но их использование в процессе разработки способствует созданию прочного фундамента, позволяющего поддерживать и развивать долгосрочные проекты. А тестирование совместной работы модулей, как вы справедливо заметили, это уже задача следующего уровня — интеграционных, нагрузочных, функциональных и прочих-прочих-прочих тестов :)
Sign up to leave a comment.