Pull to refresh

Comments 43

Давно хочу хочу опробовать тесты, сейчас как раз есть несколько небольших приложений. Но что то все ни как не могу себя заствить. А приложения растут…
Я, как человек ленивый, понял, что тесты позволяют сэкономить много времени, возможно вам просто нравится работать.
Отнюдь, я отлично осознаю практическую пользу тестов. Так же осознаю что для получения этой пользы нужно потратить время на изучение и внедрение этой технологии, а так же то что это нужно делать в ближайшее время.
Начните с тестов для сложной логики, которую неудобно тестировать руками. Как в примере с вычислением бонусов в статье. Это должен быть новый компонент, на существующую кодовую базу тесты писать может быть неудобно. Т.е. ситуация в целом такая:
  1. Вы собираетесь написать новый компонент с множеством вариантов использования
  2. Прикиньте сколько времени вам нужно будет, чтобы сформировать и протестировать все комбинации входных параметров в ручном режиме, при написании кода и при каждом последующем изменении его в будущем
  3. Осознайте, что задать все эти условия в тестах будет намного быстрее, даже с учетом изначальных вложений в написание самого теста
  4. Напишите тесты и код (лучше сначала тесты, чтобы исключить влияние вашего знания деталей имплементации на структуру теста) и удостоверьтесь, что все работает как надо
  5. Применяйте эту методику к новым компонентам и дальше


Решиться на написание теста для одного нового компонента значительно проще, чем на 100% покрытие всего приложения. И даже если на этом компоненте все и остановится, у вас будут хоть какие-то тесты. Когда (не если) компонент сломается, а тесты помогут быстрее решить проблему, задумайтесь еще раз. Удачи!
Тут как обычно, начать не сложно, сложно заниматься этим регулярно.
Спасибо за такую прекрасную статью, начал использовать тесты недавно. Действительно это экономит кучу времени, да и ошибки, которые ранее не замечал, начали проявляться в тестах.
UFO just landed and posted this here
К сожалению, с терминологией в части тестирования до сих пор большая путаница. Практически в каждой новой команде приходится находить общий язык с нуля. Лично я выделяю интеграционные тесты как противоположность юнит-тестам. Т.е. если используется хотя бы одна внешняя система (в том числе, in-memory database), то я называю такой тест интеграционным. В таком контексте приемочные тесты являются некоторой разновидностью интеграционных, которую бывает удобно выделить в рамках конкретного проекта, чтобы не путаться. Например, в моем текущем проекте есть два уровня интеграционных тестов — на одном проверяются внутренние сервисы, а на другом уже дергаются методы api. Соответственно, те которые дергают сервисы мы просто называем интеграционными, а более высокоуровневые — приемочными. Хотя в этом случае оба слоя тестов работают с in-memory базой.
UFO just landed and posted this here
Этот вопрос — один из типичных холиваров в тестировании. Как видно из моего кода, я не считаю наличие нескольких ассертов абсолютным злом. Пока весь тест целиком помещается на экран, легко читается, а ассерты по сути проверяют одно предположение — это допустимо. Часто наличие нескольких ассертов сигнализирует о том, что тест проверяет больше одного предположения. В таком случае упрятывание их в один кастомный Matcher только скрывает проблему, не решая ее. Что касается указанного фрагмента кода, по идее тут сможет помочь новый Junit с assertAll, но мне пока не довелось обкатать этот вариант в боевых условиях.
Проблема нескольких ассертов в том, что упадет первый и по остальным не будет информации. Если вас устраивает это — то в этом нет ничего осудительного. Это ваше осознанное решение. Допускать вероятность перекрывания одной ошибкой другой или нет.
Решение вы наверняка знаете — это «мягкие» ассерты.
Решение вы наверняка знаете — это «мягкие» ассерты.

Насколько я знаю, вменяемая реализация таких ассертов из коробки появилась только в JUnit 5 (assertAll), который все еще довольно редко встречается в реальных проектах. А время на переход на новые технологии, к сожалению, можно выделить далеко не всегда. В JUnit 4 нужно подключать дополнительную библиотеку, явно создавать дополнительный объект и опять же, явно вызывать на нем проверку после ассертов. Что на мой взгляд не очень удобно.
Можно для этого обойтись и без фреймворка. Собираете ошибки в числовой код через мультиплексирование, кидаете ошибку в конце, если код не равен нулю, расшифровываете код и выводите все, что пошло не так.
Использование JUnit 4 и assertj позволяет в одном месте вызвать несколько assert'ов и скомпоновать вывод. Дополнительные объекты при этом не нужны.
Да и в целом, assertj позволяет писать более читаемые тесты, но это ИМХО
Спасибо за статью!
Действительно, даются ответы на вопросы, которые мучают меня каждый раз когда пытаюсь начать писать тесты для своих проектов.
Ещё бы где нибудь научится — как объяснить своим коллегам, начальству, заказчикам и начальству заказчиков, что тесты — очень хорошая идея, их надо писать, на них стоит выделять время разработчиков.
Очень частая ошибка в интеграционных тестах — использование реальной базы данных


Ну почему сразу ошибка-то. Если пишешь какой-нибудь REST API, то очень удобно при тестировании этого реста сверятся с БД: смотреть, что были применены именно нужные изменения.

Для запусков тестов нужно будет настраивать внешнее окружение.


Во многих проектах каждому разработчику итак надо поднять свою БД. Благо, в проектах есть init и update SQL скрипты, которые сами все настроят.

Состояние внешних систем может отличаться на разных машинах перед запуском тестов.


Тесты могут очищать БД и накатывать нужные данные. В моем опыте здесь нет особых проблем с поддержкой.

Да, я согласен, что интеграционным тестам лучше не стучаться по HTTP к каким-нибудь 3d parties. Но имхо использование настоящей БД при тестировании — это удобно и естественно.
Возможно, я немного погорячился с формулировкой и бывают ситуации, когда плюсы использования реальной базы перевешивают минусы. Лично я считаю крайне важным возможность выкачать код любого проекта из репозитория и сразу же запустить его тесты из коробки без каких-либо дополнительных телодвижений. В том числе это значительно облегчает погружение и внесение доработок в незнакомые проекты. При повседневной работе над своим текущим проектом я вообще не использую реальной базы: все проверяется на h2.

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

В целом, ниже уже написали про альтернативы в виде виртуализации внешних систем. При грамотном подходе, такой вариант, безусловно, имеет право быть.
При повседневной работе над своим текущим проектом я вообще не использую реальной базы: все проверяется на h2

Не помню, какое время назад, примерно год-полтора назад. Была статья на эту тему.
Смысл её был в том, что на проде нашли странную ошибку, которая на тестах не вылезала. Т.е. код отрабатывал одинаково, но вот результаты работы были разными. Оказалось, что ошибка закралась в разном алгоритме работы H2 и Postgres.
В итоге все тесты переписали на Postgres и нашли ещё несколько неочевидных ошибок.
В общем, использования H2 нормально, но может приводить к скрытым ошибкам.
В наших проектах мы используем несколько подходов.
1. Embended Postgres. Разработчику не надо заморачиваться с поднятием БД.
2. Предварительный запуск БД в контейнере. Данный вариант позволяет собрать некоторую «эталонную» версию БД, чтобы не тратить время на создание структуры и прогон всех UPDATE скриптов.

Плюс мы приняли решение, что один тест должен уметь запускаться на одном контексте два раза. Т.е. мы знаем правила нашей системы и делаем формируем объекты с учётом этих правил. На пример, у нас есть «идентифицирующие» поля, это далеко не всегда ID. И такие поля у нас генерируются с учётом timestamp, что позволяет нам не беспокоиться об очистке контекста и отката транзакций.
1. Embended Postgres. Разработчику не надо заморачиваться с поднятием БД.
2. Предварительный запуск БД в контейнере. Данный вариант позволяет собрать некоторую «эталонную» версию БД, чтобы не тратить время на создание структуры и прогон всех UPDATE скриптов.

А вы случайно не сравнивали скорость работы тестов в таких сценариях против использования H2? Интересно, проседает ли она и насколько. Я пробовал использовать Embedded Postgres в тестах, но он значительно медленнее стартует — это сильно мешает при повседневной разработке когда постоянно гоняешь тесты из IDE. В результате в моем текущем проекте есть возможность легко подменить H2 на EmbeddedPostgres в тесте как раз на случай расследования специфичных для него проблем, но сами тесты по умолчанию работают на H2.
Сравнивал, но это было давно, и, естественно, не в пользу postgres.
EmbendedPostgres замедляет работу. Он создаёт новый инстанс на старт нового SpringContext (с запуском всех скриптов обновления). В той же статье, о которой я упомянул было написано, что ребята научились поднимать один инстанс на всю сессию тестов вне зависимости от того, сколько запуститься Spring Context-ов.
Именно по этому мы постепенно мигрируем на «локальные» БД. Это и единичный запуск скриптов обновления или вообще его отсутствие и возможность глазами посмотреть данные в БД. И возможность проверить, как произойдёт накат изменений на существующую структуру данных.
На сервере сборок поднимается Docker-контейнер с Postgres, который потом удаляется тем самым «очищая» результаты работы тестов.

Если Postgres начнёт тормозить, то всегда можно будет настроить RamDisk.
UFO just landed and posted this here

У меня был буквально месяц-два назад опыт поднятия тестового контекста с двумя разными бд: 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?
Я упоминал его в качестве альтернативного подхода. Это вопрос выбора архитектуры ваших тестов. Лично мне удобнее тестировать методы контроллеров напрямую. В частности, это нагляднее, легче переходить внутрь тестируемого класса, проще работать с отладчиком. MockMVC я использую только если мне нужно проверить что-то специфичное, например, механизм секьюрити или ошибки сериализации объектов. Каких-то значимых проблем с игнорированием этого слоя обычно не возникает. Если же в вашем случае ситуация отличается, то конечно же стоит рассмотреть альтернативные моему подходу варианты.

В моем текущем проекте, например, значительная часть слоя контроллеров (все что касается url, method, параметров запроса) вообще автоматически генерируется по swagger-спецификации и особого смысла проверять это тестами нет.

А что вы скажете по поводу подготовки данных для теста? Ну, то есть, как приводить базу данных в исходное состояние?

Как один из вариантов dbunit или обёртка для него Spring Test dbunit

В посте ничего про dbunit нет, поэтому я и поинтересовался. Но вообще dbunit такая проблема, что очень муторно писать тесты с ним. Колонки все эти руками заполнять и зачастую не в одной таблице. Тяжело.

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

Если такой подход начинает приводить к серьезным проблемам производительности, то тут уже приходится думать над частными решениями в каждом конкретном случае.
Первая часть теста — это подготовка данных с помощью билдеров и сохранение их в базу при необходимости.

Это в методе setUp? Билдеры занимаются созданием данных специально для добавления в базу? Методы сохранения данных, полученных с помощью билдеров, написаны специально для тестов и работают только в них?

Прямо в теле теста. Билдеры просто умеют создавать объекты. Их можно как передавать в качестве параметров, так и сохранять в базу. Для сохранения в базу использую дефолтную реализацию репозиториев через SpringData. Плюс в том, что никакого кода кроме объявления интерфейса в таком случае писать не нужно и можно полагаться на то, что этот функционал рабочий. Эти же репозитории используются и в продакшн-коде.

Подробности по коду и реализации можете посмотреть в моем проекте на гитхабе. В одном из примеров упомянутых в статье подготовка данных в начале теста выглядит так:

Product product = product(«productName»).build();
productRepository.save(product);

То есть получается, что у вас билдеры просто заменители sql. Сталкивались с проблемами при изменении схемы данных? Как их решили?

Хм, я правильно понял, что вы не используете ORM (Object-relational mapping) в своих задачах? Потому что в противном случае изменения схемы базы данных отразятся на изменении модели данных в проекте безотносительно тестов и описанной вами проблемы не возникнет. Если это так, то просто взять мой подход и применить не получится, придется подумать как лучше реализовать эти идеи в условиях вашего стека технологий.
Хм, я правильно понял, что вы не используете ORM (Object-relational mapping) в своих задачах?

Использую.


Потому что в противном случае изменения схемы базы данных отразятся на изменении модели данных в проекте безотносительно тестов и описанной вами проблемы не возникнет.

Если данные пользователя будут храниться в одной таблице вместо двух это автоматом приведёт к тому, что придётся переписывать билдеры и то, что они делают для тестов, которые проверяют что-то что зависит от состояния пользователя в БД. Это если вы используете спринговые репозитории, которые привязаны к определённой таблице или определённой сущности.

А, в таком случае да. Придется обновить билдеры и тесты. На моей практике такие серьезные изменения с моделью происходят редко. Гораздо чаще либо изменяется структура существующего объекта (в таком случае решается доработкой билдера в одном месте — не надо править множество скриптов), либо появляются новые объекты, что приводит к созданию новых билдеров.
UFO just landed and posted this here
Это другого уровня тесты.
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, а вот в реальности результат работы может быть другой.
Все зависит от того, что конкретно вы хотите проверять. В некотором роде это решение — часть архитектуры ваших тестов. В общем случае, чем больше слоев проверяется, тем сложнее становится написание и сопровождение тестов.

Приведенные мной тесты сосредоточены вокруг логики работы самого приложения. Я считаю, что это хорошая стартовая точка, особенно при первичном внедрении тестирования. В случае если в вашей конкретной ситуации таких проверок оказывается недостаточно (например, возникают регулярные проблемы с БД), то безусловно стоит рассмотреть другие подходы.
Абсолютно согласен с утверждением.

Но если речь про интеграционные тесты, то тут как раз надо проверять интеграцию :) и моменты с БД скрывают не очевидные подводные камни.
Я выше описал, как мы стараемся решать часть проблем с уровнем БД в интеграционных тестах.
Sign up to leave a comment.