Не так давно у меня появился интерес узнать, какие вообще нынче есть подходы и отношения к Юнит тестированию. Сделано это было скорее в познавательных целях. Сравнив труды по популярности, я решил ознакомиться с книгой "Unit Testing: Principles, Practices, and Patterns (Vladimir Khorikov)". Я не ставлю своей задачей полностью рецензировать данный труд, но не могу не обратить внимание на некоторые тезисы данной книги, которые часто вижу и слышу и о которых хотелось бы поговорить.
Личное впечатление о книге
"Unit Testing Principles, Practices, and Patterns" является систематизацией опыта автора в тестировании. Причём в тестировании именно том самом, которым стоило бы заниматься разработчикам, не тестировщикам. А о том, почему стоило бы заниматься, мы ответили чуть ранее в нашей другой статье. Книга и правда большая и, возможно, весьма полезная джунам и новичкам.
Если зайти на страницу данного труда на Amazon, то в секции "часто покупается вместе с" незамедлительно появятся книги Clean Code, что не случайность. Данная книга, определённо, написана в похожем стиле и вдохновлена Робертом Мартином, а также ставит перед собой несколько схожую задачу — выверить на личном опыте грамотный подход к разработке. Подобный стиль написания может показаться многим весьма категоричным. Параллели здесь можно проследить и с Фаулером, ведь некоторые параграфы книги весьма точно пересказывают его статью "mocks aren"t stubs". Из той же статьи происходит несколько сомнительный "-изм" "мокисты" в противоположность "классикам" тестирования. Читателю предлагается не забыть взять это на вооружение для важных споров.
Я предлагаю взглянуть глубже на некоторые тезисы из данных статей, которые я нахожу спорными, либо непонятными мне лично.
Предмет дискуссии
По сути, речь идёт о том, что из себя должен представлять Unit тест. Упомянутые авторы предпочитают почти полностью отказаться от моков как таковых, тестирую классы только в своей совокупности. В противовес же им идёт подход, где все зависимости класса полностью "мокаются", в следствии чего наш класс тестируется в полной изоляции от всего остального. Если попробовать визуализировать этот простой вопрос, то выглядело бы это следующим образом.
Я думаю, что при поиске примеров использования какой-нибудь популярной библиотеки на подобии "Mockito" можно было бы увидеть подобные примеры тестов. Но вернёмся к интересующим нас моментам.
Хрупкие тесты
Intra-system communications are implementation details because the collaborations your domain classes go through in order to perform an operation are not part of their observable behavior. These collaborations don't have an immediate connection to the client's goal. Thus, coupling to such collaborations leads to fragile tests.
Using mocks to verify communications between classes inside your system results in tests that couple to implementation and therefore fall short of the resistance-to-refactoring metric.
Хориков часто апеллирует к "хрупкости" тестов как к метрике, с которой стоит бороться. Имеется ввиду, что если тесты будут сильнее зависеть от внутренней технической имплементации классов, то это приведёт к более частым "красным" тестам. Частое возникновение такого поведения, как показано на примере, может привести к потере веры к тестам вместе с их последующими отключением и игнорированием.
Часто, сам применяя именно "мокистский" подход, я могу наблюдать обратное. Да и у меня самого желания взять и выключить "падающий" тест не возникало. Сухой инженерный подход к разработке пытается найти объяснения многим негативным практикам разработки именно во внутренней структуре написанного кода и полностью обходит стороной договорённости в коллективе, конфигурацию CI/CD, а также всевозможные поведенческие первопричины со стороны сотрудников (как то частичное или полное отсутствие интереса к тестам ввиду их непонимания). В настройках CI/CD на проектах с развитыми практиками тестирования зачастую нельзя добавить код в кодовую базу, если на проекте после изменения не проходят тесты. В таких ситуациях часто тесты запускаются автоматически на каждое изменение и в каждой ветке. А бдительность коллег на ревью гарантирует, что тесты нельзя просто выключить.
Всё это означает, что падения тестов их не обесценивают, но при некоторых обстоятельствах могут и помогать. Если код поменялся, даже если это внутреннее устройство одного лишь только класса, то поведение этого класса стоит перепроверить через тесты. Это и правда дополнительная рутина, но рутина служащая дополнительным барьером от возможных ошибок в коде. Стоит отметить, что и более высокоуровневые тесты также не всегда полностью защищены от этой проблемы, даже если она и матеарилизуется в меньшей степени. Скажем, высокоуровневые тесты могут опираться на контейнер с базой данных, что может часто привести к таймаутам на некоторых машинах и Flaky тестам.
Бизнес ценность
Tests shouldn't verify units of code. Rather, they should verify units of behaviour: something that is meaningful for the problem domain and, ideally, something that a business person can recognize as useful.
A test should tell a story about about the problem your code helps to solve, and this story should be cohesive and meaningful to a non-programmer.
Среди идеалистов разработки широко распространена поговорка, что любой код является ответственностью (liability), нежели чем ценностью (asset). Поэтому кода должно быть как можно меньше, а если уж код написали, то он должен быть как можно ценнее. А в идеальном мире, у нас должен существовать только код, выполняющий какие-то бизнесс процессы. Всё, что не нацеленно на выполнение задач бизнеса, считается прослокой либо скрепляющим более важные части кода "клеем" ("glue code"). Данный код, разумеется, должен быть минимизирован.
В обсуждениях чего-то столь высокоуровнено можно много где покривить носом, но всё же данные идеи являются в основе своей трудно оспоримыми. Не стоит забывать, что любой наш код мы пишем для достижения каких-то целей. Осознание последнего является важной частью карьерного роста разработчика. Логичным развитием данной мысли является, что и тест кейсы должны быть напрямую завязаны с бизнесс функцией приложения, фиксируя и валидируя указанные бизнес требования. Ну, то есть быть высокоуровневыми и иметь максимально возможное покрытие. А следовательно, выступать этакой своеобразной документацией к вашему коду, в том числе доступной менеджменту.
Из указанного выше также делается заключение, что тест должен представлять собой ценность для менеджмента в виде этакой динамической документации к коду.
Но давайте попробуем вспомнить, насколько часто мы слышим или видим, чтобы менеджеры проекта или же хотя бы тимлиды применяли тесты таким образом. А должно ли менеджеру или ПО, задач у которых в современном бизнесе и так много, вообще быть интересным чтение нашего кода? Сразу же также учтём, что многие менеджеры могут не разбираться в программировании как в дисциплине. Как правило, их интерес к программному продукту останавливается на том, какую бизнес задачу код выполнить может, а какую нет, и как качественно этот код это сделает. И получать такую информацию они предпочитают через несколько более высокоуровневый интерфейс — через разработчика. Ну или хотя бы через вашу борду. А разработик же в своём большинстве достаточно квалифицирован, дабы разобраться в свойствах программного кода. Очень часто такое бывает, что разбираться в этом коде дополнительно ему не приходится, ведь он сам его же и написал.
Именно в связи с этим является несколько избыточным закладывать в тестируемый код читаемость большую, нежели чем таковая нужна именно разработчику. Иначе говоря, программный код именно как код, нежели чем как продукт представляет интерес больше всего именно разработчику, в связи с чем должен быть удобен больше всего команде разработки. А потому и тест кейс на, скажем, коннектор к базе данных или же репозиторий не должен являться проблемным до тех пор, пока этот тест кейс полезен именно разработчику. Что и является первоначально хорошим поводом изолировать этот код от всего остального и протестировать отдельно.
Какой подход к тестированию лучше использовать?
На самом деле, как правило, применять можно успешно любой из этих двух подходов, либо даже их комбинацию. Превосходства каждого из них легко продемонстрировать, а вот для выявить их недостатки зачастую весьма непросто. При этом очень часто забывается, что ваша среда разработки должна быть настроена на ваше же удобство, а не чьё-то другое. Если вам просто неудобно писать через, скажем, TDD, то попробуйте найти об этом компромисс с вами и вашими же коллегами.