Я преподаю уже больше года и заметил такую вещь, ученики не понимают, как писать unit тесты. Ни в учебных задачах, ни в рабочих проектах. Прежде чем разобраться в этом, давайте посмотрим, какие проблемы позволяют решить unit тесты.
Что здесь может пойти не так? С точки зрения интерпретатора, здесь нет ошибок, код запускается и работает. Даже исключения не выбрасывает. По крайней мере, мы об этом не знаем. Пока.
Опа! Всё-таки можно сломать этот код и получить исключение. То, что мы сейчас сделали, называется деструктивный тест. «Проверка кода на прочность», найти слабые места.
Хорошо, что это даёт? А то, что теперь мы можем написать обработчик такой ситуации и выдавать более осмысленное исключение, которое уже будет содержать информацию о том, как его избежать. Обратите внимание, код даже не побывал в «боевой» production среде, а мы уже исправили ошибку! Разве не круто?
Функция, которую я привёл в пример является «чистой». То-есть, результат её работы зависит только от входных аргументов. Соответсвенно, можно что-либо испортить только путём подачи на вход неправильных аргументов.
В случае с чистыми функциями, тесты написать легче всего. Как правило, проекты на Java Spring почти всегда состоят из классов и методов. А отличие метода от класса в том, что он по-умолчанию имеет доступ к внутреннему состоянию объекта класса. То-есть, его работа зависит от какого-то скрытого для внешнего наблюдателя состояния, которое он не может контролировать. Такие методы/функции называются грязными, в терминах функционального программирования.
Что же делать? Как в таком случае писать тесты для этих грязных функций? Прежде чем ответить на этот вопрос я приведу несколько примеров грязного кода. Это может быть не только отдельно взятая подпрограмма, но даже класс и проект в целом, если он использует какие-то внешние библиотеки, базы данных, службы итд. Почему так? Потому что у вас нет полного контроля за ситуацией. Те же базы данных работают по принципу чёрного ящика. Мы можем писать туда данные. Позже их можно считать и производить другие манипуляции. Но контроля за выполнением этого функционала у вас нет. В случае с подключаемыми библиотеками, доподлинно не известно все то, что может происходить внутри, когда вы используете библиотеку. Конечно, если вы не излучили от и до исходные коды используемых библиотек. Вот пример, как сделать из функции выше “грязную”:
Теперь, при написании теста для этой функции нельзя гарантировать, что он будет срабатывать всегда, т.к новая функция div зависит от переменной divider. В данном примере, я могу сломать тест, если изменю значение divider, при том что сама функция работает корректно.
Все просто
Я привел простой пример, но в “боевых” условиях, у вас будет больше таких зависимостей. Как можно исправить функцию из предыдущего примера?
Нужно сделать функцию чистой. Для этого, я перенес переменную divider в аргументы функции. Теперь, вызывающая сторона полностью контролирует поведение функции, как и её результат. Значит, мы можем написать unit тест, который будет работать в любых условиях.
Бывают более сложные ситуации, когда таких зависимостей может быть не одна, как в случае с глобальной переменной выше, а восемь, например. В таком случае, будет неправильно и не удобно переносить их все в аргументы. Хотя технически, это возможно. Лучшим решением, на мой взгляд, применить паттерн Dependency Injection. А также, можно “упаковать” часто используемые аргументы в некий общий контекст. Например, сделать класс. В предыдущей статье есть примеры, как это сделать.
Зачем нужны тесты?
Я много говорю о грязных функциях, потому что они препятствуют написанию качественных unit тестов. Что же вообще происходит в этих тестах? Их задача – своевременно обнаружить логические ошибки, а также другие ошибки, которые не может найти компилятор или интерпретатор. Например, код на JavaScript иногда требует писать проверки типов в ручную, тогда как в Java вы не сможете использовать Boolean на месте String. Это пример простой ошибки. Но бывают ситуации, когда нет иного выхода, кроме как написать код, который зависит от тех или иных условий. Например, от порядка вызова процедур.
Приведу пример. Есть некая игра, в которой оставшиеся деньги пользователя при выходе или проигрыше распределяются поровну между остальными игроками. Чтобы это реализовать, нужно получить баланс проигравшего пользователя, а потом разделить его на количество оставшихся игроков и добавить получившуюся сумму каждому из них.
Для этой задачи критически важно, чтобы пятая строка шла после удаления пользователя на четвертой строке. Хотя можно было бы и поместить её в начало функции и она бы работала. Но неправильно.
Unit тест в данном примере может защитить от человеческого фактора, осуществляя автоматическую проверку, что все идёт своим чередом. Чтобы у будущие программисты знали нюансы работы с вашей системой без чтения документации. У вас есть документация системы, кстати? То-то же.
Используем базу данных для временных данных unit тестов
Никогда так не делайте. Максимум, использовать in memory базы, по типу SQLite или H2. Ваш тест не должен чём-либо ограничиваться. Если вы пишите в любую базу данных в тесте, значит, для запуска этих тестов нужна база данных. Все члены команды будут обязаны ее себе поставить. Подумайте, вы бы хотели устанавливать на собственную машину софт, который нужен для запуска unit тестов?
Тоже самое можно сказать о зависимостях на каком-либо сайте, веб службе, будь то бакеты на амазоне, различные cdn, распределенные хранилища, очереди. Чем больше проект, тем больше у него будет таких зависимостей. Тем тяжелее будет запустить unit тесты. Поэтому, ваша задача абстрагироваться в unit тестах от внешних сервисов, с помощью моков.
В названии статьи я упомянул тесты для Java Spring, давайте теперь посмотрим, как сделать мок базы данных в помощью Mockito.
Что такое мок?
Мок объект – это хороший пример практического применения паттерна проектирования Proxy. Его задача состоит в отслеживании подотчётных ему вызовов методов. Кроме этого, вы можете сделать stubbing, чтобы заменить вызовы «грязного» кода на что-то более приемлемое. Например, заменить обращения к базе данных на ваши собственные методы, объявленные прямо внутри unit тестов. За счет этого тесты станут независимыми от среды исполнения и их можно будет задействовать в CI/CD пайплайнах. Например:
Имеется FileService, в нем нужно протестировать метод setDeletedStatusForFileByUuid.
Я знаю, что внутри тестируемого метода должен вызываться fileRepository.save, поэтому, в тесте сделаю эту проверку.
Здесь вызов тестируемого метода происходит на девятой строке. Я знаю, что внутри него используется fileRepository, который отвечает за связь с базой данных. Чтобы подменить этот вызов, я использую статический метод when класса Mockito. В седьмой строке я указываю, какое значение должно вернуться из метода findById. Обратите внимание, что подготовить мок нужно до вызова тестируемого метода. И в заключении, я проверяю, что был вызван метод fileRepository.save. Если этого не произойдет, тест вернет ошибку.
Таким образом, мы реализовали проверку бизнес логики. В случае, если кто-то удалит вызовы методов, которые мы проверили, тесты помогут сразу выявить эту ошибку. Хотя с точки зрения компилятора — все ок. Тестировать таким образом нужно те методы, которые критичны для вас.
Как понять, что критично, а что нет?
Например, в примере выше я помечаю файл как удаленный, перед этим проверяя, что он существует в базе данных. Значит, я должен проверить, что точно происходит чтение из базы данных. Если этого, по какой-то причине, не произошло, значит, я могу с уверенностью заявить, что это ошибочное поведение. Тест должен выявлять подобные ситуации. Также, в примере выше я поставил проверку на вызов записи в базу данных обновленного файла. Опять же, если этого не произошло — это ошибка.
Я показал самые простые моки и проверки. Кроме этого, вы можете проверять возвращаемые значения с помощью assert-ов, конкретные аргументы у методов моков. Допустим, можно проверить, что метод сохранения был вызвал с одним аргументом типа String. И затем сравнить эту строку с эталоном, который возвращает рабочий код.
Самое сложное — уметь выявлять зависимости вашего кода
Ссылка на оригинал статьи:
https://abritov.medium.com/как-писать-unit-тесты-для-java-spring-и-jest-979d2b53ae76