В статье о тестировании поведения было написано буквально пару слов о сравнении фактических аргументов методов с ожидаемыми. Пришло время рассказать о этом механизме подробнее.
Stub mocks и pattern matching
Не совсем корректно называть то, о чем я сейчас напишу «pattern matching», но идея крайне близка.
Возьмём код из статьи о тестировании поведения и дополним интерфейс UserRepository:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public interface UserRepository {
/**
* Finds user by it's login.
* @param login username to search
* @return User object or null if not found.
*/
User findOne(String login);
/**
* Saves user object into database.
* @param u object to save.
*/
void save(User u);
}
|
Посмотрим, что будет, если мы определим разные варианты ответов для разных логинов:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class stubPatternMatchingTest {
@Rule
public EasyMockRule em = new EasyMockRule(this);
@Mock
private UserRepository testedObject;
@Test
public void testStubTwoVariants() {
User anotherUser = new User();
anotherUser.setGroup(testGroup());
anotherUser.setUsername("SECOND");
expect(testedObject.findOne(testUser().getUsername())).andStubReturn(testUser());
expect(testedObject.findOne(anotherUser.getUsername())).andStubReturn(anotherUser);
replay(testedObject);
assertThat(testedObject.findOne("SECOND"), is(anotherUser));
assertThat(testedObject.findOne("TEST"), is(testUser()));
}
}
|
EasyMock смотрит, с каким аргументом вызван метод и возвращает ответ, который был задан для этого аргумента. Это удобно, поскольку позволяет создавать наборы «параметр-результат» на все случаи жизни. C другой стороны, если результат всех вызовов один, а параметров много, это неудобно, поскольку требует создания таких наборов для каждого параметра.
Argument matchers to the rescue
Аргументы можно проверять не только непосредственным заданием значения аргумента, но и описательно, с использованием матчеров. И это не hamcrest матчеры, хотя они очень похожи.
В случае с проблемой, описанной выше, можно использовать матчер anyString():
1
2
3
4
5
6
7
8
9
|
@Test
public void testAnyString() {
expect(testedObject.findOne(anyString())).andStubReturn(testUser());
replay(testedObject);
assertThat(testedObject.findOne("SECOND"), is(testUser()));
assertThat(testedObject.findOne("TEST"), is(testUser()));
}
|
anyString() делает очевидную вещь — разрешает mock объекту принимать в качестве параметра любую строку. Кроме anyString() существуют any*() матчеры для всех встроенных типов. Для матчера anyObject() можно дополнительно задать класс этого самого any object. Альтернативой anyObject(Class clazz) будет матчер isA(Class clazz).
Самое интересное, что для разных вызовов можно использовать и матчеры и явно заданные аргументы. В коде ниже я задаю два варианта ответа для явно заданных аргументов и один общий вариант, для всех остальных значений аргументов:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Test
public void testStubThreeVariants() {
User anotherUser = new User();
anotherUser.setGroup(testGroup());
anotherUser.setUsername("SECOND");
expect(testedObject.findOne(testUser().getUsername())).andStubReturn(testUser());
expect(testedObject.findOne(anotherUser.getUsername())).andStubReturn(anotherUser);
expect(testedObject.findOne(anyString())).andStubReturn(testUser());
replay(testedObject);
assertThat(testedObject.findOne("SECOND"), is(anotherUser));
assertThat(testedObject.findOne("TEST"), is(testUser()));
assertThat(testedObject.findOne("STILLALIVE"), is(testUser()));
}
|
При этом, смешивать в одном вызове матчеры и непосредственно заданные параметры нельзя. Хотя если очень хочется, то можно, используя матчер eq()
1
|
expect(userRepository.findUserByLoginAndGroup(anyString(),eq(testGroup())).andStubReturn(testUser())
|
Кроме eq(), сранивающего объекты через вызов equals(), существуют и другие сравнивающие значения матчеры:
- eq() для чисел с плавающей запятой, позволяющий задать погрешность
- eqAry() для массивов. Массивы сравниваются через Arrays.equals()
- lt(),leq() операции «меньше» и «меньше или равно». Работают для всех численных типов и типов, реализующих Comparable
- ge(),geq() соответственно «больше» и «больше или равно». Ограничения те же, что и для le()/leq()
- cmpEq() сравнивает объекты вызовом Comparable.compareTo(). Ограничения те же.
- same() почти eq(), но сравнивает идентичность объектов, а не их эквивалентность.
Ещё стоит упомянуть isNull()/notNull(). Из их названия очевидно, что они проверяют 🙂
Матчеры можно группировать друг с другом:
1
2
3
4
5
6
7
8
9
|
@Test
public void testMatcherCombination() {
expect(testedObject.findOne(or(startsWith("TE"), matches("S.*")))).andStubReturn(testUser());
replay(testedObject);
assertThat(testedObject.findOne("SECOND"), is(testUser()));
assertThat(testedObject.findOne("TEST"), is(testUser()));
}
|
or() срабатывает, если по крайней мере один матчер срабатывает. Парный ему матчер and() требует срабатывания обоих матчеров. К двум логическим операциям прилагается матчер not(), который инвертирует действие своего аргумента.
В коде, показывающем как группировать матчеры, я продемонстрировал и два новых матчера, специфичных для строк: startsWith() проверяет, начинается ли аргумент с заданной подстроки; matches() (или find()) принимает регулярное выражение и накладывает его на аргумент.
Код примера доступен на github