Как правильно писать unit тесты?

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

У меня возник этот вопрос, после изучения юнит-тестов в Yii фреймворке. LoginFormTest::testLoginNoUser() обращается к методу LoginForm::login(), который внутри себя вызывает LoginForm::getUser() и процесс выполнения выходит за рамки класса LoginForm в класс User. В итоге получается, что тестируются LoginForm::login(), LoginForm::getUser() и User::findByUsername(). Но почему. Ведь я не должен хотеть здесь тестировать работу метода User::findByUsername(), я хочу делать это в другом тесте.

Можно ли это назвать юнит-тестированием или это всё же интеграционное тестирование?
Сейчас же получается, что User::findByUsername() неявно тестируется в LoginFormTest, а также будет явно тестироваться в UserTest, это нормально? И если да, то нормально ли сваливать всё в одну директорию и юнит тесты и интеграционные? Где грань.
  • Вопрос задан
  • 7475 просмотров
Решения вопроса 1
He11ion
@He11ion
PHP-monkey
Обычно есть mock-и и stub-ы для задач именно юнит-тестирования.
Да, это очень похоже на интеграционное, впрочем, оно не критично отличается от юнит, так что большой проблемы тут не вижу.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 3
@thyratr0n
Непонятно, что вы хотели получить в ответ, ибо не задали четкого вопроса.

Во-первых, вы, конечно, "удачно" выбрали Yii первой версии в качестве примера. Эта фреймворка не очень приспособлена для изготовления "тестопригодного" кода, ибо содержит на всех стадиях функционирования большое количество "магии". Не верите? Попробуйте что-то наваять на базе CActiveRecord и запилить юнит-тесты. У вас получится что угодно, но не то, что надо.
Самый главный аспект юнит-тестирования состоит в том, что один тест (метод класса-теста) должен решать строго одну задачу, а не проверять в фоне целую подсистему.
На одном из своих проектов я решил эту проблему, написав целую свору заглушек.
Во-вторых, все зависит от задач. Грубо говоря, юнит-тесты проверяют код как есть. Следующий уровень тестов (не берусь его называть, ибо я их постоянно путаю) проверяет бизнес-сценарии. И самый высокий уровень проверяет все это дело в связке с внешними интерфейсами.
И в-третьих, один тест не должен включать код других тестов, ибо каждый тест должен быть изолирован, считая, что весь остальной код работает. Именно поэтому, когда вы пишете, например, тест для мелкой модельки, не нужно гонять работу с БД: считается, что вся подсистема взаимодействия с БД _работает_.

Да, и еще. Очень часто путают моки и стабы.
Стаб - это заглушка - код, который "глушит" выполнения другого. Цель заглушки - проверка того, _что_ возвращает объект в своей работе. В вашем случае нужен какой-то объект, который не позволит коду выполняться далее LoginForm::login(). Как это сделать? При статической типизации кода - никак. Нужна надстройка, которая будет динамически использовать данные объекты, чтобы их можно было заменить на стабы.
Мок - это надстройка над тестируемым объектом, целью которой является проверка того, _как_ работает объект внутри, именно поэтому моки всегда строятся на Reflection.
Ответ написан
@matperez
По хорошему, в юнит тестах нужно использовать заглушки вместо других частей системы, которые вы не хотите сейчас тестировать. Эти заглушки нужно подсовывать в тестируемую модель либо через контейнер DI на этапе конфигурации тестового окружения, либо через явный метод в модели.

Пример:
В LoginForm можно выделить явную зависимость от чего-то вроде UserFinder, который может найти пользователя по email или login. Вместо него можно подсунуть некий объект возвращающий вполне конкретную модель пользователя.
Тестировать модель пользователя в этом тесте нам тоже не хочется, поэтому UserFinder должен вернуть не реальную модель User, а опять же некую заглушку, возвращающую наперед заданные данные.

Проще всего для создания заглушек использовать моки из phpunit. Через эту штуку так же можно мокать отдельный метод всего класса. К примеру можно сделать, что бы метод findUser в модели LoginForm сразу отдавал "правильного" пользователя и тем самым убрать из рассмотрения UserFinder.

В общем, какой код - такие и тесты. Если код позволяет легко подставить зависимости, можно его тестировать в юнит стиле, иначе получаются недоинтерационные тесты. Соответственно, если будете писать тесты до самого кода, само собой будет получаться более тестируемый код.
Ответ написан
nonlux
@nonlux
В интернете пишут, что тестируемый метод нужно всегда изолировать от любых зависимостей, иначе тестируемый метод в процессе своего выполнения подтянет внешние методы этого же или другого класса, что противоречит юнит-тестированию, так как в таком случае будет тестироваться не одна сущность, а взаимодействие между несколькими сущностями.
Верно ли это утверждение?


Я считаю в целом верно. Что бы эту решить проблему придумали дубли (вот гугл на русском показал).

Т.е если писать чистый юнит-тест для LoginForm, то оптимальным решением на мой взгляд заменить на "моки":
- YII:$app->:$app->user, который вернет для Yii::$app->user->login()false
- app\models\User , который вернет для User::findByUsername()null

К сожалению, с YII не работаю и поэтому более детальною реализацию подобного сейчас написать не могу.

Можно ли это назвать юнит-тестированием или это всё же интеграционное тестирование?
И если да, то нормально ли сваливать всё в одну директорию и юнит тесты и интеграционные? Где грань.


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

Если же писать сначала код, а потом его покрывать тестами они уже больше похожи на интеграционные, как вы говорите. Собственно так и есть в вашем примере.

Я лично для себя пришел к выводу что подобная теоретическая мишура мне мешает и я не выделяю тесты на такие категории. Для себя я выделяю тесты которые тестируют бизнес-логику приложения и тесты (спецификации) для кода.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы