В предыдущих статьях о EasyMock все тесты были написаны в стиле чёрного ящика: проверялось что функция возвращает определённый результат для заданных значений, а как она эта делает, никого не интересует. Mock-объекты использовались как средство изоляции кода и не более того.
Между тем, вопрос «как функция что-то делает» не менее важен, чем вопрос «что функция делает». Например метод, который создаёт пользователя, сохраняет его в базу данных и возвращает свежесозданного пользователя вызывающему коду:
1 2 3 4 5 6 7 8 9 | public User create(String login) { User result = new User(); result.setLogin(login); //some other processing userRepository.save(result); return result; } |
Написать тест к этому методу на первый взгляд несложно: делаем nice mock для userRepository и проверяем, чтобы возвращался правильно созданный пользователь. Проблема с этим тестом выявится гораздо позднее, когда через несколько итераций кто-нибудь удалит вызов userRepository.save(), а тест продолжит работать нормально и того, что функция фактически перестала быть работоспособной, никто не заметит. Следовательно в этом случае тест должен проверять не только результат работы метода, но и процесс его работы, то есть поведение.
Подготовка
Как обычно, нужен пустой maven проект, с JUnit, Hamcrest и EasyMock.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <junit.version>4.12</junit.version> <hamcrest.version>1.3</hamcrest.version> <easymock.version>3.3.1</easymock.version> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-library</artifactId> <version>${hamcrest.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.easymock</groupId> <artifactId>easymock</artifactId> <version>${easymock.version}</version> </dependency> </dependencies> |
Нам также понадобиться модель приложения: пара классов сущностей, пара интерфейсов репозиториев итд:
Тест без проверки поведения
Для начала напишем тест, который не проверяет поведения, чтобы было с чем сравнивать в дальнейшем.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public class UserServiceNiceTest { @Rule public EasyMockRule em = new EasyMockRule(this); @Mock(type = MockType.NICE) private GroupRepository groupRepository; @Mock(type = MockType.NICE) private UserRepository userRepository; @TestSubject private UserService testedObject = new UserService(); @Test public void testNiceMock() { replay(groupRepository, userRepository); assertThat(testedObject.create("TEST"), is(testUser())); } @Test public void testSavelessNiceMock() { replay(groupRepository, userRepository); assertThat(testedObject.savelessCreate("TEST"), is(testUser())); } } |
Тест, как и ожидалось не проваливается ни для create() ни для ошибочного savelessCreate().
Проверка аргументов вызова
Изменим тип mock’а с nice на default и посмотрим что будет:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | @Mock(type = MockType.DEFAULT) private GroupRepository groupRepository; @Mock(type = MockType.DEFAULT) private UserRepository userRepository; @TestSubject private UserService testedObject = new UserService(); //This test will fail intentionally @Test public void testNiceMock() { replay(groupRepository, userRepository); assertThat(testedObject.create("TEST"), is(testUser())); } @Test public void testDefaultMock() { groupRepository.save(testGroup()); userRepository.save(testUser()); replay(groupRepository, userRepository); assertThat(testedObject.create("TEST"), is(testUser())); } |
Тест, в котором поведение не задано, предсказуемо проваливается:
1 2 3 4 5 6 7 8 | testNiceMock(ru.easyjava.easymock.service.UserServiceDefaultTest) Time elapsed: 0.001 sec <<< FAILURE! java.lang.AssertionError: Unexpected method call GroupRepository.save(ru.easyjava.easymock.entity.Group@273c92): at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:44) at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:94) at com.sun.proxy.$Proxy8.save(Unknown Source) at ru.easyjava.easymock.service.UserService.create(UserService.java:30) at ru.easyjava.easymock.service.UserServiceDefaultTest.testNiceMock(UserServiceDefaultTest.java:37) |
Как я и писал раньше, default mocks выбрасывают AssertionError при вызове метода, который не был предусмотрен ранее. Однако EasyMock кроме факта вызова ещё и проверяет какой аргумент был передан вызываемому методу и совпадает ли он с заданным аргументом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | //This test will fail intentionally @Test public void testWrongUserMock() { User wrongUser = new User(); wrongUser.setGroup(testGroup()); wrongUser.setUsername("WRONG"); groupRepository.save(testGroup()); userRepository.save(wrongUser); replay(groupRepository, userRepository); assertThat(testedObject.create("TEST"), is(testUser())); } |
Совпадение ожидаемого и фактического аргумента производится через equals(), поэтому мне и пришлось писать собственные equals()/hashCode() в классах сущностей. Кроме прямого совпадения можно использовать матчеры в стиле hamcrest, но о них я расскажу в отдельной статье. Да, тест, конечно же, проваливается:
1 2 3 4 5 6 7 8 9 | testWrongUserMock(ru.easyjava.easymock.service.UserServiceDefaultTest) Time elapsed: 0.001 sec <<< FAILURE! java.lang.AssertionError: Unexpected method call UserRepository.save(ru.easyjava.easymock.entity.User@4e7924): UserRepository.save(ru.easyjava.easymock.entity.User@517b21f): expected: 1, actual: 0 at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:44) at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:94) at com.sun.proxy.$Proxy9.save(Unknown Source) at ru.easyjava.easymock.service.UserService.create(UserService.java:35) at ru.easyjava.easymock.service.UserServiceDefaultTest.testWrongUserMock(UserServiceDefaultTest.java:62) |
Проверка фактического вызова
Что ж, мы теперь знаем, что EasyMock может требовать явного задания поведения mock’а и проверяет параметры. Но начинали то мы с факта вызова и самое время проверить, что будет если ожидаемый вызов не произойдёт?
1 2 3 4 5 6 7 8 9 | @Test public void testSavelessMock() { groupRepository.save(testGroup()); userRepository.save(testUser()); replay(groupRepository, userRepository); assertThat(testedObject.savelessCreate("TEST"), is(testUser())); } |
Удивительное рядом — вызова нет, а тест не провалился! Почему? Потому что EasyMock не знает, когда можно проверять все ли вызовы сделаны и поэтому ему надо указывать необходимость проверки явно, используя метод verify()
1 2 3 4 5 6 7 8 9 10 11 | @Test public void testVerifyMock() { groupRepository.save(testGroup()); userRepository.save(testUser()); replay(groupRepository, userRepository); assertThat(testedObject.savelessCreate("TEST"), is(testUser())); verify(groupRepository, userRepository); } |
В этот раз всё как и ожидалось:
1 2 3 4 5 6 7 | testVerifyMock(ru.easyjava.easymock.service.UserServiceDefaultTest) Time elapsed: 0.024 sec <<< FAILURE! java.lang.AssertionError: Expectation failure on verify: UserRepository.save(ru.easyjava.easymock.entity.User@4e7924): expected: 1, actual: 0 at org.easymock.internal.MocksControl.verify(MocksControl.java:241) at org.easymock.EasyMock.verify(EasyMock.java:2100) at ru.easyjava.easymock.service.UserServiceDefaultTest.testVerifyMock(UserServiceDefaultTest.java:84) |
Количество вызовов методов
Если вы внимательно читали вывод ошибок, то увидели, что в каждой ошибке присутствует счётчик вызовов: expected: 1, actual: 0. EasyMock проверяет не только сам факт вызова метода, но и количество ожидаемых вызовов методов и количество фактически сделанных вызовов методов. Счётчик ведётся отдельно для каждого метода и для каждого набора аргументов этого метода. Например, если вызов userRepository.save() делается три раза для трёх разных пользователей, это можно описать как:
1 2 3 4 | userRepository.save(user1); userRepository.save(user2); userRepository.save(user3); replay(userRepository); |
В этом случае EasyMock будет ожидать, что метод save() вызовут по одному разу с тремя разными аргументами.
В случае, когда набор аргументов один, а вызовов несколько, можно не копировать строки по количеству вызовов, а явно задать их число:
1 2 | userRepository.save(user) expectLastCall().times(3) |
В этом случае 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’ов, их придётся создавать вручную:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public class UserServiceStrictTest { IMocksControl ctrl = createStrictControl(); private GroupRepository groupRepository = ctrl.createMock(GroupRepository.class); private UserRepository userRepository = ctrl.createMock(UserRepository.class); private UserService testedObject = new UserService(userRepository, groupRepository); @Before public void setUp() { ctrl.reset(); } @Test public void testStrictMock() { groupRepository.save(testGroup()); userRepository.save(testUser()); ctrl.replay(); assertThat(testedObject.create("TEST"), is(testUser())); ctrl.verify(); } @Test public void testWrongOrderMock() { userRepository.save(testUser()); groupRepository.save(testGroup()); ctrl.replay(); assertThat(testedObject.create("TEST"), is(testUser())); ctrl.verify(); } } |
Обратите внимание, что методы replay() и verify() вызываются у объекта IMockControl, в котором и созданы mock-объекты. Таких контролов может быть несколько, позволяя тестировать порядок вызовов внутри групп mock’ов.
Послесловие
В этом примере я применил подход, известный как ObjectMother: создание тестовых объектов вынесено в отдельный класс, который используется одновременно всеми тестами.
Этот подход решает три проблемы в разработке тестов:
- Код тестов становится лаконичнее и понятнее.
- Все тесты используют гарантированно идентичные тестовые объекты
- При внесении изменений в код тестовый объект достаточно будет исправить в одном единственном месте.
Рекомендую 🙂
Код примера доступен на github.