В предыдущих статьях о 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.