Многим знакома ситуация, когда в проекте есть какие-то тесты, которые либо проходят успешно, либо нет. Такие тесты называются flaky, и в этой статье мы поговорим о том, как избежать создания таких тестов.
В качестве примера я буду использовать Java Spring Framework, но обсуждаемые здесь причины актуальны для любой среды.
1. Нестабильная среда
Наиболее распространенной причиной нестабильных тестов является нестабильная среда.
Например, в тестах используется некоторая общая база данных, развернутая для всех тестов. При параллельном запуске нескольких заданий сборки в конвейерах CI/CD тесты изменяют данные друг друга.
Наиболее надежным решением в этом случае является изоляция сред. Например, вы можете запустить базу данных в контейнере docker (в Java для этого есть популярная библиотека — Testcontainers). Таким образом, один тестовый прогон будет использовать исключительно свою базу данных, а после тестового прогона завершит ее.
Также важно не забывать очищать состояние после каждого теста, чтобы не повлиять на следующие тесты в наборе тестов.
2. Планировщики и отложенные операции
Более редкая причина — использование планировщиков или отложенных операций.
Представьте, что в какой-то части приложения мы объявили планировщик:
А потом, во время тестового прогона, неожиданно сработало, потому что именно в это время запускался тест. Разработчик, написавший тест, не ожидал такого побочного эффекта.
Чтобы избежать такого поведения, вы можете настроить расписание. Это упростит поддержку приложения (не нужно пересобирать код для изменения расписания). Это также позволит вам переопределить расписание в тестах, чтобы планировщики запускались только вручную.
А потом в application.yml в тестах:
Если ваше приложение использует отложенные операции, вам нужно быть с ними еще более осторожными. Действие может быть зарегистрировано в одном тесте, а само выполнение может происходить во время выполнения другого теста.
3. Использование сна
Использование Thread.sleep(..) для ожидания завершения какого-либо действия указывает на то, что с тестами что-то не так.
Тайм-аут может быть недостаточным по какой-то причине (например, пауза GC). Если мы установим тайм-аут с большим запасом, это резко замедлит выполнение теста.
Вместо sleep мы можем возвращать Future в асинхронных операциях, а ждать используя Future.get(20, TimeUnit.SECONDS) с большим таймаутом. Таким образом, общее выполнение теста даже уменьшится.
4. Переключение контекста
Если вы используете Spring, то переключение между кешированными контекстами может быть причиной ненадёжных тестов. Такое поведение сложно отладить, и оно может произойти в любой момент после добавления другого тестового класса.
Например, если один тестовый класс в наборе использует @MockBean, а другой нет, будут созданы 2 разных контекста. И движок JUnit будет так или иначе переключаться между ними.
Я знаю только один гарантированный способ избежать этого — объединение тестовых классов в наборы тестов, чтобы все тесты в одном наборе использовали один и тот же контекст приложения.
Заключение
Ненадежные тесты значительно портят опыт разработки приложения. Некоторые компании создают специальные сервисы для обнаружения ненадежных тестов и перезапускают только их.
Конечно, мой список неполный, поэтому буду рад, если вы напишите в комментариях причины, с которыми пришлось столкнуться.