Тестирование поведения

Behaviour-Management-BLOG-WEBВ предыдущих статьях о EasyMock все тесты были написаны в стиле чёрного ящика: проверялось что функция возвращает определённый результат для заданных значений, а как она эта делает, никого не интересует. Mock-объекты использовались как средство изоляции кода и не более того.

Между тем, вопрос «как функция что-то делает» не менее важен, чем вопрос «что функция делает». Например метод, который создаёт пользователя, сохраняет его в базу данных и возвращает свежесозданного пользователя вызывающему коду:

Написать тест к этому методу на первый взгляд несложно: делаем nice mock для userRepository  и проверяем, чтобы возвращался правильно созданный пользователь. Проблема с этим тестом выявится гораздо позднее, когда через несколько итераций кто-нибудь удалит вызов userRepository.save(), а тест продолжит работать нормально и того, что функция фактически перестала быть работоспособной, никто не заметит. Следовательно в этом случае тест должен проверять не только результат работы метода, но и процесс его работы, то есть поведение.

Подготовка

Как обычно, нужен пустой maven проект, с JUnit, Hamcrest и EasyMock.

Нам также понадобиться модель приложения: пара классов сущностей, пара интерфейсов репозиториев итд:

Тест без проверки поведения

Для начала напишем тест, который не проверяет поведения, чтобы было с чем сравнивать в дальнейшем.

Тест, как и ожидалось не проваливается ни для create()  ни для ошибочного savelessCreate().

Проверка аргументов вызова

Изменим тип mock’а с nice на default и посмотрим что будет:

Тест, в котором поведение не задано, предсказуемо проваливается:

Как я и писал раньше, default mocks выбрасывают AssertionError при вызове метода, который не был предусмотрен ранее.  Однако EasyMock кроме факта вызова ещё и проверяет какой аргумент был передан вызываемому методу и совпадает ли он с заданным аргументом:

Совпадение ожидаемого и фактического аргумента производится через equals(), поэтому мне и пришлось писать собственные equals()/hashCode() в классах сущностей. Кроме прямого совпадения можно использовать матчеры в стиле hamcrest, но о них я расскажу в отдельной статье. Да, тест, конечно же, проваливается:

Проверка фактического вызова

Что ж, мы теперь знаем, что EasyMock может требовать явного задания поведения mock’а и проверяет параметры. Но начинали то мы с факта вызова и самое время проверить, что будет если ожидаемый вызов не произойдёт?

Удивительное рядом — вызова нет, а тест не провалился! Почему? Потому что EasyMock не знает, когда можно проверять все ли вызовы сделаны и поэтому ему надо указывать необходимость проверки явно, используя метод verify()

В этот раз всё как и ожидалось:

Количество вызовов методов

Если вы внимательно читали вывод ошибок, то увидели, что в каждой ошибке присутствует счётчик вызовов:  expected: 1, actual: 0. EasyMock проверяет не только сам факт вызова метода, но и количество ожидаемых вызовов методов и количество фактически сделанных вызовов методов. Счётчик ведётся отдельно для каждого метода и для каждого набора аргументов этого метода. Например, если вызов userRepository.save() делается три раза для трёх разных пользователей, это можно описать как:

В этом случае EasyMock будет ожидать, что метод save() вызовут по одному разу с тремя разными аргументами.

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

В этом случае EasyMock будет ожидать, что метод save() вызовут с одним и тем же аргументом три раза.

Кроме times() существуют и другие методы для задания числа ожидаемых вызовов:

  • atLeastOnce() — метод должен быть выван не менее одного раза
  • anyTimes() — метод может быть вызван любое количество раз ( в том числе 0 раз)
  • once() — метод должен быть вызван ровно один раз.
  • times(min, max) — метод должен быть вызван не менее min раз и не более max раз включительно.

expect() vs expectLastCall()

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

Метод expect() принимает в себя вызов не void функции mock’а и позволяет задать возвращаемый результат; возвращаемое исключение; количество вызовов и тд. expectLastCall() делает тоже самое для void функций, кроме возвращаемого результата, конечно 🙂  В остальном их использование идентично.

Проверка порядка вызова

Создание mock’а с MockType.STRICT включит проверку порядка вызова для этого mock’а, но если проверку надо производить для нескольких mock’ов, их придётся создавать вручную:

Обратите внимание, что методы replay() и verify() вызываются у объекта IMockControl, в котором и созданы mock-объекты. Таких контролов может быть несколько, позволяя тестировать порядок вызовов внутри групп mock’ов.

Послесловие

В этом примере я применил подход, известный как ObjectMother: создание тестовых объектов вынесено в отдельный класс, который используется одновременно всеми тестами.

Этот подход решает три проблемы в разработке тестов:

  • Код тестов становится лаконичнее и понятнее.
  • Все тесты используют гарантированно идентичные тестовые объекты
  • При внесении изменений в код тестовый объект достаточно будет исправить в одном единственном месте.

Рекомендую 🙂

Код примера доступен на github.