Почему ваши тесты должны проверять только наблюдаемое поведение, а не детали реализации

Привет и добро пожаловать в новый выпуск серии «Давайте понюхаем тесты».

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

Давайте начнем!

Когда ваши тесты хотят знать слишком много

Когда-то, до того, как я углубился в тему автоматизированного тестирования, это случалось со мной много раз. Что именно? Ну, на всякий случай, я хотел убедиться, что мои тесты подтвердили даже больше вещей, чем было необходимо. Раньше я считал, что чем больше assert и подобных утверждений включают в себя мои тесты, тем большую ценность они приносят.

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

Хрупкие испытания? Наблюдаемое поведение? Детали реализации?

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

Отличным источником знаний о передовой практике тестирования является книга под названием «Модульное тестирование: принципы, практика и шаблоны», написанная Владимиром Хориковым.

По словам автора (глава 4.4.4), тесты становятся хрупкими, когда

они не выдерживают рефакторинга и станут красными независимо от того, нарушена ли базовая функциональность.

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

Что же такое наблюдаемое поведение? В главе 5.2.1. автор книги определяет наблюдаемое поведение следующими словами:

Чтобы фрагмент кода был частью наблюдаемого поведения системы, он должен выполнять одно из следующих действий:

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

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

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

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

Этот подход должен дать вам более четкое различие между наблюдаемым поведением и деталями реализации.

Практический пример: таблица лидеров

Давайте подробнее рассмотрим следующий пример, написанный на Java:

  • мы разрабатываем таблицу лидеров для игры, назовем ее «Chase and Race»
  • мы хотим, чтобы наша таблица лидеров возвращала лучшего игрока на основе набранных очков

Класс Player отвечает за хранение имени игрока и его счета. Оценка обновляется с помощью функции Player#updateScore.

Класс Leaderboard позволяет нам добавлять игроков в список лидеров с помощью функции Leaderboard#addPlayer и получать лучшего игрока игры с помощью Leaderboard#getBestPlayer.

В классе LeaderboardTesttest мы проверяем, может ли метод Leaderboard#getBestPlayer вернуть игрока с наибольшим количеством очков:

Пока все хорошо — как видите, отчет об испытаниях зеленый.

Позже мы решили реорганизовать внутренности класса Leaderboard, чтобы он сортировал список игроков в порядке убывания всякий раз, когда мы добавляем в него нового игрока:

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

Как видите, тестовый отчет стал красным, но наблюдаемое поведение осталось нетронутым — функция Leaderboard#getBestPlayer по-прежнему работает правильно, возвращает игрока с наибольшим количеством очков.

Как это исправить? Было бы лучше, если бы однажды написанный тест не требовал от нас дополнительного внимания в случае рефакторинга кодовой базы. Для этого список Leaderboard#players должен быть недоступен извне, поэтому пометить эту коллекцию модификатором private будет достаточно:

Но как насчет теста? Теперь у него ошибка компиляции:

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

Заключение

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

Резюмируем, что можно и что нельзя:

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

Рекомендации

Модульное тестирование: принципы, практика и шаблоны, Владимир Хориков