Как сделать юнит тесты
Плагин для IntelliJ IDEA полностью бесплатен для разработки программ с открытым исходным кодом. Чтобы установить его, перейдите в меню “Настройки” (Preferences) IntelliJ IDEA, в его левой части выберите Плагины (Plugins) и найдите через поиск Diffblue Cover , как показано ниже.
После завершения установки и при наличии существующего проекта, который импортирован в IntelliJ IDEA, плагин начнет индексировать и анализировать всю кодовую базу (т. е. классы, зависимости и т. д.) в качестве фоновой задачи. Это может занять некоторое время в зависимости от её размера и сложности.
Вы также можете дополнительно настроить плагин Diffblue Cover через меню IntelliJ IDEA, чтобы подогнать его под свои нужды.
Если вам хочется поэкспериментировать, рекомендую начать с базового проекта. Ниже приведен пример проекта Spring Boot с контроллером, уровнем обслуживания, репозиторием, использующим данные spring, и резидентной (In-Memory)базой данных:
Можете спокойно клонировать репозиторий и делать с ним, что угодно.
Шаг 2. Краткое руководство по проекту
Если вы клонировали проект-пример, приведенный выше, то этот раздел для вас. Здесь я кратко покажу классы, для которых мы собираемся создавать юнит-тесты.
Слой контроллера
Вот как выглядит контроллер Spring Boot, который мы собираемся протестировать:
Как показано выше, у нас есть несколько конечных точек для получения списка всех студентов, а также добавления, получения и удаления по идентификатору каждого в отдельности.
Сервисный слой
Слой сервиса соединяет контроллер со слоем репозитория, где будет находиться большая часть логики.
Дальше мы попытаемся сгенерировать модульные тесты для вышеперечисленных классов с помощью плагина Diffblue Cover.
Шаг 3. Генерация юнит-тестов при помощи плагина, основанного на искусственном интеллекте
Именно здесь происходит вся магия. Другими словами, плагин, работающий на основе ИИ, вступает в дело и самостоятельно, без какой-либо помощи, генерирует юнит-тесты.
Если вы уже установили плагин, щелкните правой кнопкой мыши по StudentController и выберите опцию “Написать тесты” ( Write Tests ) в меню действий, как показано выше.
Плагин начнет анализировать контроллер и каждый из его методов. Наконец, он сгенерирует юнит-тесты для каждого из методов.
Генерация тестов занимает какое-то время — Diffblue Cover анализирует класс и его методы. Но затраты времени минимальны по сравнению с тем, сколько понадобится для написания вручную.
Ниже приведен тест StudentControllerTest, сгенерированный Diffblue Cover.
Тесты выглядят очень надежными и следуют лучшим практикам Spring Boot относительно тестирования.
Все сгенерированные юнит-тесты целиком можно найти в этом пулл-реквесте .
Шаг 4. Тестовое покрытие сгенерированных юнит-тестов
Инструмент покрытия кода или тестового покрытия показывает процент кода, охваченного тестами.
Запустим его на сгенерированных ранее тестах, чтобы получить представление о том, какой процент кода покрывается этими тестами.
Мы получили довольно хороший процент покрытия по классам, для которых были сгенерированы тесты, то есть по пакетам Controller, service и util.
Стоит отметить, что это крайне простая кодовая база без сложного кода или логики, и плагин, похоже, прилично справляется с работой по созданию ряда надежных модульных тестов. В случае устаревшего кода или запутанной базы плагин может вести себя по-другому.
Демонстрация по созданию юнит-тестов с плагином Diffblue Cover
Для тех, кому интересно, я приготовил также быструю видео-демонстрацию процесса: начиная от установки плагина до генерации тестов и оценки покрытия для сгенерированных тестов.
Быстрое демо по генерации как юнит-тестов на Java с помощью Diffblue Cover
Недостатки автоматизированной генерации юнит-тестов
Несмотря на многие преимущества, которые дает генерация юнит-тестов, у нее есть несколько недостатков, которые я хотел бы вкратце затронуть.
Во-первых, я полагаю, что разработчик, который пишет юнит-тесты, лучше, чем разработчик, который этого не делает.
Во-вторых, на мой взгляд, юнит-тестирование косвенно влияет на то, как мы пишем код. Если мы пишем код, который плохо спроектирован, то модульное тестирование становится еще более неприятным процессом. К примеру, представим метод, который делает десять разных вещей, и к тому же в нем содержится много вложенных условий и довольно сложная логика. Было бы непросто написать тесты, которые охватывают все пути выполнения таких методов.
Однажды, когда я приводил аргументы в пользу юнит-тестирования, один разработчик сказал мне:
Я трачу 20% своего времени на написание кода и 80% на написание юнит-тестов для этого кода.
Если код, который вы пишете, нелегко читать и понимать, он сильно связан и мало сопряжен, то модульное тестирование окажется очень трудоемким и неэффективным процессом.
Заключение
В целом я остался доволен работой плагина и качеством сгенерированных юнит-тестов для проекта-образца.
Однако я заметил, что плагин не может генерировать тесты для определенных классов и в некоторых случаях пропускает пути выполнения (ветви). В видео-примере выше есть дополнительная информация на этот счет.
Чтобы написать хороший юнит-тест, надо задать критерии его хорошести и придерживаться их. Это, вообще, относится к любой области деятельности: определяем цель, движемся к ней, если результат совпал с целью, значит всё удалось 🙂
Признаки хорошего юнит-теста
Первый тест удовлетворяет условию фальсифицируемости: если заменить sum ( 2 , 2 ) на sum ( 3 , 3 ) , тест провалится. Однако придумать замену для второго теста, чтобы он провалился, не представляется возможным. Замена может быть и умозрительной, например для примера теории о положительности чётных степеней натуральных чисел, такую замену придумать несложно (i*i=-1 где i — мнимая единица), но реализовать не получится — тип Integer не комплексный.
- Одна проверка на один тест. Для этого есть несколько причин: во-первых обычно юнит-тест завершается на первой же провалившейся проверке, тем самым скрывая будущие провалы последующих проверок. Во-вторых так проще обеспечивать идентичность и нетронутость тестовых данных. В третьих, так проще отлаживать. В четвёртых для простых функций можно писать и больше проверок на один тест, если это приемлимо.
- Тест использует возможности тестового фреймворка для сброса данных в предопределённое состояние перед запуском. Тест не использует результаты других тестов и не полагается на изменения в тестовых данных, которые они вносят. И на порядок исполнения тоже не полагается 🙂
- Тест проверяет только код, с которым взаимодействует напрямую. Никаких вызовов методов из других классов быть не должно: исполнятся должен только код класса, метод которого тестируется, все остальные зависимости заменены дублёрами.
- Тест не проверяет те аспекты кода, которые не влияют на его поведение (если только вы не тестируете именно эти аспекты). Пример:
При написании этого теста нет необходимости проверять, что там в log . trace ( ) передали, потому что на поведение функции это не влияет (если только вы не отлаживаете log . trace ( ) )
Если вы практикуете test driven development, этот вопрос не имеет смысла: тесты появляются ещё раньше кода. Впрочем, заметная часть разработчиков пишет тесты post faсtum или покрывает тестами более старый код. В этом случае:
Модульный тест – это категория тестов с самой высокой степенью детализации, основанная на тестовой пирамиде. Обычно он ориентирован на функциональность класса, функции или компонента пользовательского интерфейса и изолирован от внешней системы, такой как базы данных и сторонние API.
Зачем утруждать себя написанием модульных тестов
В большинстве случаев задача написания/рефакторинга кода заключается в том, чтобы убедиться, что вы не нарушаете существующую функциональность. Раньше разработчику нужно было проверить измененный класс/функцию вручную, чтобы убедиться, что ничего не сломалось. Ручная работа подвержена ошибкам. Разработчики могут забыть некоторые тестовые случаи, и код с багами отправляется в продакшен. Наличие модульных тестов и их правильная настройка на CI избавит вас от таких сценариев. Следовательно, это повысит вашу уверенность в CD до продакшена.
Обнаруживайте ошибки как можно раньше
Модульные тесты пишутся (и должны быть написаны) изолированно, поэтому их можно выполнять без необходимости разворачивать внешние службы и запускать такие инструменты, как puppeteer. Он может работать быстро и гораздо менее требователен к памяти по сравнению с сквозными тестами. Это уникальное свойство модульных тестов позволяет разработчикам выполнять тесты столько, сколько необходимо в процессе разработки.
Некоторые тестранеры, такие как jest, предоставляют возможность наблюдать за запуском тестов каждый раз, когда в код вносятся изменения, что еще больше облегчает обнаружение ошибок во время разработки.
Документация
Тщательно написанные тесты могут выступать в качестве документации, поскольку они описывают желаемое поведение конкретной части программного обеспечения. Я также нахожу, что тесты очень полезны во время процесса проверки кода. Они дают рекомендации по поведению программного обеспечения и избавляют от необходимости подробно разбираться в деталях реализации, чтобы понять его функциональность.
Краткое введение в разработку на основе тестирования (TDD)
Разговор о модульных тестах не будет полным без упоминания Test-Driven Development (TDD). В двух словах TDD можно охарактеризовать как красно-зеленый-рефакторинг подход к разработке программного обеспечения.
- Вы начинаете с написания одного теста, чтобы охватить одно требование. Тест должен быть провален, так как у вас нет работающей реализации системы (красный).
- Вы пишете реализацию, чтобы она прошла тест (зеленый).
- Отрефакторите свой код (если это необходимо).
- Переходите к следующему требованию и возвращайтесь к шагу 1
Ключевой вывод из TDD заключается в том, чтобы позволить тестам управлять вашей архитектурой, а не наоборот.
Практика написания модульных тестов
Я буду использовать образец, написанный на javascript + jest, так как это язык и тестовый фреймворк, с которыми мне наиболее комфортно. В качестве примера мы используем класс dateFormatter со следующей спецификацией:
- Этот класс имеет публичный метод format, которая принимает объект Javascript Date в качестве входных данных и возвращает строку даты в формате dd-mm-yyyy.
- Если входные данные являются недопустимым объектом даты, он вызовет исключение.
Пишем осмысленное имя теста
Золотое правило содержательного названия теста – это четкое описание выходных и входных данных. Читатель должен быть в состоянии понять желаемое поведение без необходимости читать детали реализации тестируемой системы.
Каждый тест должен охватывать только 1 сценарий
Следует избегать тестирования двух функций в рамках одного теста. Причина этого принципа заключается в том, что если тест проваливается, мы не знаем, какая функция проваливается. Вам нужно будет проверить обе функции, даже если только одна из них не пройдет тест.
Используйте шаблон AAA
Шаблон Arrange, Act, Assert – это распространенный шаблон, который можно использовать для улучшения читабельности теста, разделяя части теста пустой строкой.
- Arrange готовит необходимые приспособления, насмешки, заглушки и тестируемую систему.
- Act выполняет тестируемую функциональность
- Assert-это утверждение результата выполнения относительно желаемого значения
Изолируйте свой юнит от внешних зависимостей
Допустим, мы добавили еще одну функциональность поверх класса. Каждый раз, когда метод format выполняется, он будет регистрировать результат в стороннем API с помощью функции logToExernalAPI.
Как написать тест для этой новой функциональности? Один из подходов заключается в рефакторинге класса и использовании инъекции зависимостей, чтобы избежать прямой зависимости от другого блока. Используя эту технику, мы также улучшаем дизайн класса, отделяя его от реализации logger.
Избегайте тестирования детализированной реализации
Пример детализации реализации тестирования выглядит следующим образом:
- Проверка последовательности вызовов функций
- Проверка внутреннего состояния класса
Следует избегать деталей реализации тестирования, поскольку это создает тесную связь между тестами и реализацией. Например, если вы пишете тесты для проверки последовательности вызовов функций внутри метода вашего класса и решаете изменить порядок, тест не будет выполнен, даже если он на самом деле не влияет на пользователя вашего класса.
Работа над устаревшим кодом: должен ли я сначала отрефакторить или написать тест?
В некоторых случаях конкретный класс/компонент, с которым вы хотите работать, написан таким образом, что его очень трудно проверить.
В общем, я бы предложил написать тест перед рефакторингом, если это не очень трудно сделать.
Вывод
Написание модульных тестов является важной и широко распространенной практикой повышения качества программного обеспечения, предоставляя средства для обеспечения правильности программного обеспечения и позволяя разработчику обнаруживать ошибки как можно раньше.
В конце концов, эта техника (как и другие техники) потребует некоторой практики и дисциплины для овладения. Я надеюсь, что эта статья может дать вам некоторое базовое представление о том, как и почему писать хорошие тесты для вашего программного обеспечения.
Сейчас поговорим о юнит-тестировании с применением JavaScript-фреймворка Jest.
Тестирование в жизни
Понять важность Тестирования как процесса, а не только в ИТ-сфере, поможет пример из жизни. Представим, что есть бывший в употреблении автомобиль, с виду приличный. Его владелец утверждает, что автомобиль “как новый, на 10 из 10”. Конечно, надо проверить автомобиль перед покупкой, и глупо будет если этого не сделать. Так же и с софтом.
Понимание, как и зачем тестируется софт, полезно и разработчику, особенно если речь идет о юнит-тестах. Это улучшает его эффективность, дает понимание когда и почему софт не работает, или неудобен для клиента, помогает писать быстрый, хорошо структурированный код.
Юнит-тестирование
Юнит-тестирование (также называемое “модульным”) – это, как следует из второго названия, подвид тестирования, который сосредотачивается на тестировании отдельных модулей кода.
Этот фреймворк разработан Facebook, что уже должно о чем-то говорить.
Как утверждают создатели фреймворка из Jest Core Team, “он позволяет быстро писать легкие, удобные и функциональные тесты с отличным API”
Приступаем к установкам и настройкам
1. Устанавливаем npm
Он может быть уже установлен, и чтобы проверить это, вводим в терминале npm –version. Если не установлен, идем сюда и читаем документацию, как установить npm.
2. Создаем package.json
Это можно сделать двумя способами. Или инициализация после команды npm init создаст package.json, но для этого надо сначала кое-что настроить, то есть ввести своё описание, присвоить название, и возможно ввести еще что-то дополнительное. Второй путь: ввод команды npm init -y, чтобы проскочить этот этап конфигурации, и тогда получим файл дефолтной конфигурации.
3. Устанавливаем Jest
Вводим в терминале npm install –save-dev jest. При этом автоматически пропишется нужная зависимость.
Далее идем к файлу package.json, находим в нем, в начале, раздел “scripts” и меняем в нем значение “test” на “jest”.
Все готово к тестированию.
Матчеры
Перед тем как погрузиться в процесс, нужно понять, что такое матчеры (matchers).
Как утверждается в документации, Jest применяет “матчеры” для проверки тестовых значений в различных ситуациях.
Перейдем к практике.
toBe
Матчер toBe означает полную эквивалентность. Другими словами “===”.
Такой тест пройдет успешно.
А вот этот тест выше – упадет, даже если значения те же – потому что массивы имеют разные адреса в памяти. (Если этот нюанс вызывает затруднения, надо почитать что-то базовое об управлении памятью в JavaScript).
Как видим, тестируя массивы (или объекты), применять toBe – не лучшая опция. Для этого существует матчер удобнее – toEqual.
toEqual
Этот матчер почти всегда подойдет. Он означает то что называется “глубокая эквивалентность” (так называемая “deep equality”), то есть он попарно сопоставляет каждое значение в объектах или массивах. Поэтому если в предыдущем тесте применить toEqual вместо toBe, то тест пройдет.
toMatch
Сравнивая строки с регулярными выражениями, применяем toMatch.
toContain
Если хотим проверить, входит ли элемент в последовательность, применяем матчер toContain.
Тестирование на противоположность матчеру
Если нужно протестировать на противоположность, просто пишем .not перед матчером: если это матчер toBe, то пишем: .not.toBe(…) .
Посмотреть весь список матчеров и опций можно здесь – в документации по Jest.
А теперь тестируем
Процесс тестирования достаточно прост.
- Допустим, у нас есть функция, принимающая массив из строк, и выбирающая из них самую длинную:
Обращаем внимание на module.exports = longestStr в конце. Это для экспорта результата функции; так мы можем потом импортировать его в файл теста.
- Создаем отдельный JavaScript-файл, который при работе с функциями рекомендуется назвать так же как функция которую собираемся протестировать, и расширение ему желательно иметь или .spec.js, или .test.js. Оба расширения подходят.
- В файле из пункта 2, импортируем нашу функцию:
- Теперь пишем свой тест. В нем пишем test(), получающий 2 параметра: первый это строка, описывающая что код будет делать.
И второй параметр – это функция в которую мы будем писать две вещи: 1) функцию называемую expect, в которую будем передавать функцию с проверяемыми аргументами. 2) наш матчер. Вот так:
- Запускаем тест, вводом команды npm test в терминале.
Тогда увидим что-то такое:
Если тест пройдет, увидим такое:
Если тест не пройдет, то увидим результат выполнения. (В данном примере мы специально изменили кое-что в в файле longestString.js, чтобы тест выдал fail):
Теперь попробуем несколько тестов
Если надо запустить только один файл с тестами, надо указать его название при запуске npm test (и не забыть добавить расширение):
- Например у нас есть еще файл sum.js
- Если запустить npm test, оба тестовых файла запустятся одновременно, и если один из них упадет, можно запутаться. Поэтому указываем, какой файл запустить: npm test sum.spec.js
Пропускаем ненужные тесты
Итак, если у нас есть несколько тестов, в одном файле .spec.js или test.js, то при запуске npm test fileName все тесты запустятся одновременно. Если надо запустить лишь один отдельный тест, и чтобы не надо было удалять или закомментировать другие, используем метод .skip:
Запустится только один тест, остальные будут пропущены, однако попадут в лог:
На этом пока все, удачи в юнит-тестировании! Посмотри соответствующий раздел на сайте – там бывает полезное.
До запуска приложения в производство, когда оно станет доступно пользователям, важно убедиться, что данное приложение функционирует, как и должно, что в нем нет ошибок. Для проверки приложения мы можем использовать различные схемы и механизмы тестирования. Одним из таких механизмов являются юнит-тесты.
Юнит-тесты позволяют быстро и автоматически протестировать отдельные компоненты приложения независимо от остальной его части. Не всегда юнит-тесты могут покрыть весь код приложения, но тем не менее они позволяют существенно уменьшить количество ошибок уже на этапе разработки.
Мы не должны тестировать код используемого фреймворка или используемых зависимостей. Тестировать надо только тот код, который написали мы сами.
Надо отметить, что в целом концепция юнит-тестов не является непреложным требованием к веб-разработке, да и вообще к разработке. Кто-то считает, что юнит-тесты обязательно должны покрывать весь код проекта, кто-то полагает, что юнит-тесты можно использовать преимущественно для особо сложных моментов в коде приложения, какой-то сложной логики. Некоторые не используют юнит-тесты.
Но тем не менее юнит-тесты несут потенциальные преимущества при разработке, к которым следует отнести не только собственно проверку результата и тестирование кода, но и другие, как например, написание слабосвязанных компонентов в соответствии с принципами SOLID. Ведь чтобы тестировать компоненты приложения независимо друг от друга, нам надо, чтобы они были слабосвязанными. А подобное построение приложения в дальнейшем может положительно сказаться на его последующей модификации и поддержке.
Большинство юнит-тестов так или иначе имеют ряд следующих признаков:
Тестирование небольших участков кода ("юнитов")
Для создания юнит-тестов выбираются небольшие участки кода, которые надо протестировать. Тестируемый участок, как правило, должен быть меньше класса. В большинстве случаев тестируется отдельный метод класса или даже часть функционала метода. Упор на небольшие участки позволяет довольно быстро писать простенькие тесты.
Тестирование в изоляции от остального кода
При тестировании важно изолировать тестируемый код от остальной программы, с которой он взаимодействует, чтобы потом четко определить возможность ошибок именно в этом изолированном коде. Что упрощает и повышает контроль над отдельными компонентами программы.
Создание юнит-тестов для небольших участков кода ведет к тому, что количество этих юнит-тестов становится очень велико. Если процесс получения результатов и проведения тестов не автоматизирован, то это может привести к непроизводительному расходу рабочего времени и снижению производительности. Поэтому важно, чтобы результаты юнит-тестов представляли собой простое решение, означающее, пройден тест или нет. Для автоматизации процесса разработчики обычно обращаются к фреймворкам юнит-тестирования
Тестирование только общедоступных конечных точек
Даже небольшие изменения в классе могут привести к неудаче многих юнит-тестов, поскольку реализация используемого класса изменилась. Поэтому при написании юнит-тестов ограничиваются только общедоступными конечными точками, что позволяет изолировать юнит-тесты от многих деталей внутренней реализации компонента. В итоге уменьшается вероятность, что изменения в классах могут привести к провалу юнит-тестов.
Фрейморки тестирования
Для написания юнит-тестов мы можем сами создавать весь необходимый функционал, использовать какие-то свои способы тестирования, однако, как правило, для этого применяются специальные фреймворки. Некоторые из них:
Данные фреймворки предоставляют несложный API, который позволяет быстро написать и автоматически проверить тесты.
Разработка через тестирование (Test-Driven-Development)
Отдельно стоит сказать о концепции TDD или разработка через тестирование . TDD представляет процесс применения юнит-тестов, при котором сначала пишутся тесты, а потом уже программный код, достаточный для выполнения этих тестов.
Использование TDD позволяет снизить количество потенциальных багов в приложении. Создавая тесты перед написанием кода, мы тем самым описываем способ поведения будущих компонентов, не связывая себя при этом с конкретной реализацией этих тестируемых компонентов (тем более что реализация на момент создания теста еще не существует). Таким образом, тесты помогают оформить и описать API будущих компонентов.
Порядок написания кода при TDD довольно прост:
Запускаем его и видим, что он завершился неудачей (программный код ведь еще не написан)
Пишем некоторое количество кода, достаточное для запуска теста
Снова запускаем тест и видим его результаты
Этот цикл повторяется снова и снова, пока не будет закончена работа над программным кодом. Так как большинство фреймворков юнит-тестирования помечают неудавшиеся тесты с красного цвета (например, выводится текст красного цвета), а удачный тест отмечается зеленым цветом (опять же выводится текст зеленого цвета), то данный цикл часто называют красным/зеленым циклом.
Читайте также: