(java || kotlin) && devOps
367 subscribers
6 photos
1 video
6 files
307 links
Полезное про Java и Kotlin - фреймворки, паттерны, тесты, тонкости JVM. Немного архитектуры. И DevOps, куда без него
Download Telegram
Всем привет!

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

1) должен быть быстрый набор тестов, который можно запускать после каждого изменения в коде. Почему это важно: после небольшого изменения в коде всегда понятна причина падения. После нескольких изменений подряд - уже сложнее. Быстрый - понятие нечеткое, но пойдем от обратного - 10 минут точно не быстро) 5 - тоже многовато IMHO. Идеально - минута, две.

2) как я уже писал - тесты должны быть антихрупкими. Хрупкие тесты с течением времени приводят к такому же результату, как и их отсутствие. Тесты часто падают, их отключают или не запускают, рефакторинг делать страшно, код объявляется legacy и переписывается.
Как этого можно добиться:
- не писать тесты на код, который постоянно меняется. Это один из возможных вариантов, не панацея!) Если это не бизнес-логика - это допустимо. В этом случае модульные тесты можно заменить на интеграционные, проверящие более высокоуровневый результат, которые реже меняется.
- не проверять в тесте детали реализации, тестировать результат, который потребитель тестируемого метода ожидает от него. Хорошая статья на эту тему - https://habr.com/ru/company/jugru/blog/571126/ Тестируя только результат мы теряем точность теста, но увеличиваем антихрупкость. Это необходимый компромис. Исключение: сложные утилитные методы, где алгоритм - порядок вызовов - важен.

3) покрытие кода тестами - не панацея. С одной стороны покрытие 30-50% - плохо, т.к. показывает, что много кода не покрыто тестами. С другой стороны покрытие 100% не говорит, что код хороший. Почему:
- не добавляяя Assert, добавив E2E тесты и закрыв их в try catch можно достичь очень хороших результатов по покрытию)
- важно различать Line Coverage и Condition (Branch) coverage. Первое считает процент покрытых срок, второе - процент протестированных путей, по которым можно прийти от начала к концу метода. В случае SonarQube тут все хорошо - он считает свою метрику, которая совмещает обе https://docs.sonarqube.org/latest/user-guide/metric-definitions/ В теории если покрыты все условия, то и по строчкам кода должно быть 100%. Или в проекте есть код, который не используется. В общем метрике SonarQube можно верить)
- предположим мы написали на первый взгляд полноценный набор тестов, с Assert-ми, все как положено. Покрытие 100% по новому коду. Но есть метод с побочным эффектом - он не только возвращает результат, но и сохраняет его внутри класса для отчетности или сравнения в будущих вызовах. Если этого не знать \ забыть - получим неполноценный тест. Конечно, есть Single Responsibility, неожиданные побочные эффекты это плохо, при TDD забыть про только что написанный код сложно, но в других случаях ситация может быть на практике. Другой кейс - тестируемый метод может вызывать библиотечную функцию, внутренности который мы не знаем. Соответственно, все возможные комбинации параметров для нее тоже не знаем. Не факт, что все такие комбинации нужно тестировать в конретном тесте - мы же не тестируем внешние библиотеки. Но факт, что какие-то важные кейсы для нашего бизнес-процесса можно упустить.

4) принцип Single Responsibility для теста звучит так: тест отвечает за одну бизнес-операцию, единицу поведения. Соотношение 1 тестовый класс = 1 тестируемый объект - не правило. Соответственно, в названии тестового класса не обязательно привязываться к названию тестируемого класса, тем более, что его в будущем могут отрефакторить.

5) ну и финальный момент - серебрянной пули нет, перед написанием тестов надо думать, что и как тестировать и выбирать наилучщий вариант.

P.S. Также хочу посоветовать хорошую книгу про тесты от автора статьи из 2) - https://habr.com/ru/company/piter/blog/528872/

#unittests #books