Comments 43
- Вы собираетесь написать новый компонент с множеством вариантов использования
- Прикиньте сколько времени вам нужно будет, чтобы сформировать и протестировать все комбинации входных параметров в ручном режиме, при написании кода и при каждом последующем изменении его в будущем
- Осознайте, что задать все эти условия в тестах будет намного быстрее, даже с учетом изначальных вложений в написание самого теста
- Напишите тесты и код (лучше сначала тесты, чтобы исключить влияние вашего знания деталей имплементации на структуру теста) и удостоверьтесь, что все работает как надо
- Применяйте эту методику к новым компонентам и дальше
Решиться на написание теста для одного нового компонента значительно проще, чем на 100% покрытие всего приложения. И даже если на этом компоненте все и остановится, у вас будут хоть какие-то тесты. Когда (не если) компонент сломается, а тесты помогут быстрее решить проблему, задумайтесь еще раз. Удачи!
Решение вы наверняка знаете — это «мягкие» ассерты.
Решение вы наверняка знаете — это «мягкие» ассерты.
Насколько я знаю, вменяемая реализация таких ассертов из коробки появилась только в JUnit 5 (assertAll), который все еще довольно редко встречается в реальных проектах. А время на переход на новые технологии, к сожалению, можно выделить далеко не всегда. В JUnit 4 нужно подключать дополнительную библиотеку, явно создавать дополнительный объект и опять же, явно вызывать на нем проверку после ассертов. Что на мой взгляд не очень удобно.
Да и в целом, assertj позволяет писать более читаемые тесты, но это ИМХО
Действительно, даются ответы на вопросы, которые мучают меня каждый раз когда пытаюсь начать писать тесты для своих проектов.
Ещё бы где нибудь научится — как объяснить своим коллегам, начальству, заказчикам и начальству заказчиков, что тесты — очень хорошая идея, их надо писать, на них стоит выделять время разработчиков.
Очень частая ошибка в интеграционных тестах — использование реальной базы данных
Ну почему сразу ошибка-то. Если пишешь какой-нибудь REST API, то очень удобно при тестировании этого реста сверятся с БД: смотреть, что были применены именно нужные изменения.
Для запусков тестов нужно будет настраивать внешнее окружение.
Во многих проектах каждому разработчику итак надо поднять свою БД. Благо, в проектах есть init и update SQL скрипты, которые сами все настроят.
Состояние внешних систем может отличаться на разных машинах перед запуском тестов.
Тесты могут очищать БД и накатывать нужные данные. В моем опыте здесь нет особых проблем с поддержкой.
Да, я согласен, что интеграционным тестам лучше не стучаться по HTTP к каким-нибудь 3d parties. Но имхо использование настоящей БД при тестировании — это удобно и естественно.
Вдобавок, в тех проектах, что я встречал, использование реальной базы часто провоцирует людей на создание дополнительных условий для запуска теста (например, тест может рассчитывать, что в базе уже лежат какие-то тестовые данные). В случае когда база виртуальная, создать себе похожие проблемы значительно сложнее.
В целом, ниже уже написали про альтернативы в виде виртуализации внешних систем. При грамотном подходе, такой вариант, безусловно, имеет право быть.
При повседневной работе над своим текущим проектом я вообще не использую реальной базы: все проверяется на h2
Не помню, какое время назад, примерно год-полтора назад. Была статья на эту тему.
Смысл её был в том, что на проде нашли странную ошибку, которая на тестах не вылезала. Т.е. код отрабатывал одинаково, но вот результаты работы были разными. Оказалось, что ошибка закралась в разном алгоритме работы H2 и Postgres.
В итоге все тесты переписали на Postgres и нашли ещё несколько неочевидных ошибок.
В общем, использования H2 нормально, но может приводить к скрытым ошибкам.
В наших проектах мы используем несколько подходов.
1. Embended Postgres. Разработчику не надо заморачиваться с поднятием БД.
2. Предварительный запуск БД в контейнере. Данный вариант позволяет собрать некоторую «эталонную» версию БД, чтобы не тратить время на создание структуры и прогон всех UPDATE скриптов.
Плюс мы приняли решение, что один тест должен уметь запускаться на одном контексте два раза. Т.е. мы знаем правила нашей системы и делаем формируем объекты с учётом этих правил. На пример, у нас есть «идентифицирующие» поля, это далеко не всегда ID. И такие поля у нас генерируются с учётом timestamp, что позволяет нам не беспокоиться об очистке контекста и отката транзакций.
1. Embended Postgres. Разработчику не надо заморачиваться с поднятием БД.
2. Предварительный запуск БД в контейнере. Данный вариант позволяет собрать некоторую «эталонную» версию БД, чтобы не тратить время на создание структуры и прогон всех UPDATE скриптов.
А вы случайно не сравнивали скорость работы тестов в таких сценариях против использования H2? Интересно, проседает ли она и насколько. Я пробовал использовать Embedded Postgres в тестах, но он значительно медленнее стартует — это сильно мешает при повседневной разработке когда постоянно гоняешь тесты из IDE. В результате в моем текущем проекте есть возможность легко подменить H2 на EmbeddedPostgres в тесте как раз на случай расследования специфичных для него проблем, но сами тесты по умолчанию работают на H2.
EmbendedPostgres замедляет работу. Он создаёт новый инстанс на старт нового SpringContext (с запуском всех скриптов обновления). В той же статье, о которой я упомянул было написано, что ребята научились поднимать один инстанс на всю сессию тестов вне зависимости от того, сколько запуститься Spring Context-ов.
Именно по этому мы постепенно мигрируем на «локальные» БД. Это и единичный запуск скриптов обновления или вообще его отсутствие и возможность глазами посмотреть данные в БД. И возможность проверить, как произойдёт накат изменений на существующую структуру данных.
На сервере сборок поднимается Docker-контейнер с Postgres, который потом удаляется тем самым «очищая» результаты работы тестов.
Если Postgres начнёт тормозить, то всегда можно будет настроить RamDisk.
У меня был буквально месяц-два назад опыт поднятия тестового контекста с двумя разными бд: mysql и postgres. Оказалось, что Embedded версия mysql работает приотвратнейше, причём зависит от ОС (даже между разными версиями win). Переписал на поднятие обоих субд в контейнерах через testcontainers. Ещё медленнее, чем embedded. Стал чаще смотреть на результат сборки в гитлабе, а на локали интеграционные пропускаю. Если сломалось, чиню, иногда переписываю историю ветки, если там много коммитов с правками.
Изначально на этом проекте тесты написаны над HashMap (эмуляция БД). Т.к. это маленький микросервис, то сложных операций нет: сохранить в таблицу с автоинкрементом, поиск по одному/двум полям. Тесты генерируют малое количество данных, так что такой подход вполне корректен :)
Итак есть отдельный maven модуль, в котором только тесты. Вся бизнес логика в других модулях. Компилирование бизнес-логики в текущих расчётах не учитывается.
В модуле dao объявлено два liqubase changeSet. Добавление таблицы и добавление новых полей в таблицу(эти скрипты естественно не запускаются, если работаю над HashMap).
Есть 5 классов с тестами. Первый класс с пустым тестом, для инициализации SpringContext. В остальных 4х классах 20 тестов. Для каждого варианта делал три запуска тестов. Перед запуском тестов делал mvn clean. Время выполнение брал из логов maven.
HashMap:
— Инициализация: 4.7s
— Тесты: 1.8s
— Maven package: 20.5s
HSQLDB (memory):
— Инициализация: 10.7s
— Тесты: 2.7s
— Maven package: 29.8s
Postgres (in docker):
— Инициализация: 9,9s
— Тесты: 3,1s
— Maven package: 26,9s
Со своей стороны замечу что отвязка от внешних систем рекомендуемая у вас имеет альтернативы в виде виртуализации внешних систем, контейнеризации и их комбинаций, ещё есть вариант подмены тестовыми аналогами, в спринге это кажется будут тестовые аспекты которые вы используете в тестовом контексте чтоб например работать с h2db вместо sql server.
Product createdProduct = productController.createProduct(product);
Не лучше ли взять для тестиования котроллеров Spring mockMVC?
В моем текущем проекте, например, значительная часть слоя контроллеров (все что касается url, method, параметров запроса) вообще автоматически генерируется по swagger-спецификации и особого смысла проверять это тестами нет.
А что вы скажете по поводу подготовки данных для теста? Ну, то есть, как приводить базу данных в исходное состояние?
Как один из вариантов dbunit или обёртка для него Spring Test dbunit
Если такой подход начинает приводить к серьезным проблемам производительности, то тут уже приходится думать над частными решениями в каждом конкретном случае.
Первая часть теста — это подготовка данных с помощью билдеров и сохранение их в базу при необходимости.
Это в методе setUp? Билдеры занимаются созданием данных специально для добавления в базу? Методы сохранения данных, полученных с помощью билдеров, написаны специально для тестов и работают только в них?
Подробности по коду и реализации можете посмотреть в моем проекте на гитхабе. В одном из примеров упомянутых в статье подготовка данных в начале теста выглядит так:
Product product = product(«productName»).build();
productRepository.save(product);
То есть получается, что у вас билдеры просто заменители sql. Сталкивались с проблемами при изменении схемы данных? Как их решили?
Хм, я правильно понял, что вы не используете ORM (Object-relational mapping) в своих задачах?
Использую.
Потому что в противном случае изменения схемы базы данных отразятся на изменении модели данных в проекте безотносительно тестов и описанной вами проблемы не возникнет.
Если данные пользователя будут храниться в одной таблице вместо двух это автоматом приведёт к тому, что придётся переписывать билдеры и то, что они делают для тестов, которые проверяют что-то что зависит от состояния пользователя в БД. Это если вы используете спринговые репозитории, которые привязаны к определённой таблице или определённой сущности.
1. В современных движках разработки UI есть свои тесты, включая проверку работы компонентов. Это отдельная тема.
2. Тесты на производительность не запускаются один раз. Т.к. производительность, это не одна операция в единицу секунды. А 10^x операций в течении определённого периода времени.
Test
public void getProduct_twoProductsInDb_correctProductReturned() {
Product product1 = product(«product1»).build();
Product product2 = product(«product2»).build();
productRepository.save(product1);
productRepository.save(product2);
Product result = productController.getProduct(product1.getId());
assertEquals(«product1», result.getName());
}
Очень стрёмный тест, если честно. Если предположить, что в БД есть ограничение на длину строки = 3 символам, то тест может отрабатывать корректно, за счёт кэширования данных на уровне ORM, а вот в реальности результат работы может быть другой.
Приведенные мной тесты сосредоточены вокруг логики работы самого приложения. Я считаю, что это хорошая стартовая точка, особенно при первичном внедрении тестирования. В случае если в вашей конкретной ситуации таких проверок оказывается недостаточно (например, возникают регулярные проблемы с БД), то безусловно стоит рассмотреть другие подходы.
Как наконец-то начать писать тесты и не пожалеть об этом