Перейти к основному содержимому

Особенности Mockito в Java 8

· 7 мин. чтения

1. Обзор

Java 8 представила ряд новых замечательных функций, таких как лямбда-выражение и потоки. И, естественно, Mockito использовал эти последние инновации во второй основной версии .

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

2. Мокирующий интерфейс с методом по умолчанию

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

Mockito версии 1 не был готов к этому изменению. В основном потому, что он не позволял нам просить его вызывать настоящие методы из интерфейсов.

Представьте, что у нас есть интерфейс с двумя объявлениями методов: первое — это старомодная сигнатура метода, к которой мы все привыкли, а другое — совершенно новый метод по умолчанию :

public interface JobService {

Optional<JobPosition> findCurrentJobPosition(Person person);

default boolean assignJobPosition(Person person, JobPosition jobPosition) {
if(!findCurrentJobPosition(person).isPresent()) {
person.setCurrentJobPosition(jobPosition);

return true;
} else {
return false;
}
}
}

Обратите внимание, что метод по умолчанию assignJobPosition() содержит вызов нереализованного метода findCurrentJobPosition() .

Теперь предположим, что мы хотим протестировать нашу реализацию assignJobPosition() без написания фактической реализации findCurrentJobPosition() . Мы могли бы просто создать фиктивную версию JobService, а затем указать Mockito вернуть известное значение из вызова нашего нереализованного метода и вызвать настоящий метод при вызове assignJobPosition() :

public class JobServiceUnitTest {

@Mock
private JobService jobService;

@Test
public void givenDefaultMethod_whenCallRealMethod_thenNoExceptionIsRaised() {
Person person = new Person();

when(jobService.findCurrentJobPosition(person))
.thenReturn(Optional.of(new JobPosition()));

doCallRealMethod().when(jobService)
.assignJobPosition(
Mockito.any(Person.class),
Mockito.any(JobPosition.class)
);

assertFalse(jobService.assignJobPosition(person, new JobPosition()));
}
}

Это вполне разумно, и сработает отлично, учитывая, что мы использовали абстрактный класс вместо интерфейса.

Однако внутренняя работа Mockito 1 была просто не готова к этой структуре. Если бы мы запустили этот код с Mockito до версии 2, мы бы получили эту красиво описанную ошибку:

org.mockito.exceptions.base.MockitoException:
Cannot call a real method on java interface. The interface does not have any implementation!
Calling real methods is only possible when mocking concrete classes.

Mockito делает свою работу и сообщает нам, что не может вызывать настоящие методы для интерфейсов, так как эта операция была немыслима до Java 8.

Хорошая новость заключается в том, что, просто изменив версию Mockito, которую мы используем, мы можем устранить эту ошибку. Используя Maven, например, мы могли бы использовать версию 2.7.5 (последнюю версию Mockito можно найти здесь ):

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.7.5</version>
<scope>test</scope>
</dependency>

Нет необходимости вносить какие-либо изменения в код. В следующий раз, когда мы запустим наш тест, ошибка больше не возникнет.

3. Вернуть значения по умолчанию для опций и потока

Необязательный и Stream — другие новые дополнения Java 8. Одно сходство между двумя классами заключается в том, что оба имеют специальный тип значения, представляющий пустой объект. Этот пустой объект позволяет избежать вездесущего исключения NullPointerException .

3.1. Пример с необязательным

Рассмотрим сервис, который внедряет JobService, описанный в предыдущем разделе, и имеет метод, вызывающий JobService#findCurrentJobPosition() :

public class UnemploymentServiceImpl implements UnemploymentService {

private JobService jobService;

public UnemploymentServiceImpl(JobService jobService) {
this.jobService = jobService;
}

@Override
public boolean personIsEntitledToUnemploymentSupport(Person person) {
Optional<JobPosition> optional = jobService.findCurrentJobPosition(person);

return !optional.isPresent();
}
}

Теперь предположим, что мы хотим создать тест, чтобы проверить, что если у человека нет текущей работы, он имеет право на пособие по безработице.

В этом случае мы заставим findCurrentJobPosition() возвращать пустое значение Optional . До Mockito 2 от нас требовалось имитировать вызов этого метода:

public class UnemploymentServiceImplUnitTest {

@Mock
private JobService jobService;

@InjectMocks
private UnemploymentServiceImpl unemploymentService;

@Test
public void givenReturnIsOfTypeOptional_whenMocked_thenValueIsEmpty() {
Person person = new Person();

when(jobService.findCurrentJobPosition(any(Person.class)))
.thenReturn(Optional.empty());

assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(person));
}
}

Эта инструкция when(…).thenReturn(…) в строке 13 необходима, потому что Mockito возвращает значение по умолчанию для любых вызовов метода для фиктивного объекта — null . Версия 2 изменила это поведение.

Поскольку мы редко обрабатываем нулевые значения при работе с Optional, теперь Mockito по умолчанию возвращает пустое значениеOption . Это то же самое значение, что и возврат вызова Optional.empty() .

Итак, при использовании Mockito версии 2 мы могли бы избавиться от строки 13, и наш тест все равно был бы успешным:

public class UnemploymentServiceImplUnitTest {

@Test
public void givenReturnIsOptional_whenDefaultValueIsReturned_thenValueIsEmpty() {
Person person = new Person();

assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(person));
}
}

3.2. Пример с потоком

То же самое происходит, когда мы имитируем метод, который возвращает Stream .

Давайте добавим новый метод в наш интерфейс JobService , который возвращает Stream, представляющий все должности, на которых человек когда-либо работал:

public interface JobService {
Stream<JobPosition> listJobs(Person person);
}

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

public class UnemploymentServiceImpl implements UnemploymentService {

@Override
public Optional<JobPosition> searchJob(Person person, String searchString) {
return jobService.listJobs(person)
.filter((j) -> j.getTitle().contains(searchString))
.findFirst();
}
}

Итак, предположим, что мы хотим должным образом протестировать реализацию searchJob(), не беспокоясь о написании listJobs() , и предположим, что мы хотим протестировать сценарий, когда человек еще не работал ни на одной работе. В этом случае нам бы хотелось, чтобы listJobs() возвращал пустой Stream .

До Mockito 2 нам нужно было имитировать вызов listJobs() , чтобы написать такой тест:

public class UnemploymentServiceImplUnitTest {

@Test
public void givenReturnIsOfTypeStream_whenMocked_thenValueIsEmpty() {
Person person = new Person();
when(jobService.listJobs(any(Person.class))).thenReturn(Stream.empty());

assertFalse(unemploymentService.searchJob(person, "").isPresent());
}
}

Если мы обновимся до версии 2 , мы можем отказаться от вызова when(…).thenReturn(…) , потому что теперь Mockito по умолчанию будет возвращать пустой Stream для имитируемых методов :

public class UnemploymentServiceImplUnitTest {

@Test
public void givenReturnIsStream_whenDefaultValueIsReturned_thenValueIsEmpty() {
Person person = new Person();

assertFalse(unemploymentService.searchJob(person, "").isPresent());
}
}

4. Использование лямбда-выражений

С помощью лямбда-выражений Java 8 мы можем сделать операторы более компактными и удобными для чтения. При работе с Mockito два очень хороших примера простоты, обеспечиваемой лямбда-выражениями, — это ArgumentMatchers и пользовательские ответы .

4.1. Комбинация Lambda и ArgumentMatcher

До Java 8 нам нужно было создать класс, реализующий ArgumentMatcher , и написать наше пользовательское правило в методеmatches() .

В Java 8 мы можем заменить внутренний класс простым лямбда-выражением:

public class ArgumentMatcherWithLambdaUnitTest {

@Test
public void whenPersonWithJob_thenIsNotEntitled() {
Person peter = new Person("Peter");
Person linda = new Person("Linda");

JobPosition teacher = new JobPosition("Teacher");

when(jobService.findCurrentJobPosition(
ArgumentMatchers.argThat(p -> p.getName().equals("Peter"))))
.thenReturn(Optional.of(teacher));

assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(linda));
assertFalse(unemploymentService.personIsEntitledToUnemploymentSupport(peter));
}
}

4.2. Комбинация лямбда и пользовательского ответа

Такого же эффекта можно добиться при комбинировании лямбда-выражений с Mockito’s Answer .

Например, если бы мы хотели имитировать вызовы метода listJobs() , чтобы заставить его возвращать поток , содержащий одно задание JobPosition , если имя человека — «Питер», и пустой поток в противном случае, нам пришлось бы создать класс (анонимный или внутренний), реализующий интерфейс Answer .

Опять же, использование лямбда-выражения позволяет нам написать все фиктивное поведение в строке:

public class CustomAnswerWithLambdaUnitTest {

@Before
public void init() {
when(jobService.listJobs(any(Person.class))).then((i) ->
Stream.of(new JobPosition("Teacher"))
.filter(p -> ((Person) i.getArgument(0)).getName().equals("Peter")));
}
}

Обратите внимание, что в приведенной выше реализации нет необходимости во внутреннем классе PersonAnswer .

5. Вывод

В этой статье мы рассмотрели, как совместно использовать новые функции Java 8 и Mockito 2 для написания более чистого, простого и короткого кода. Если вы не знакомы с некоторыми функциями Java 8, которые мы здесь видели, ознакомьтесь с некоторыми из наших статей:

Также проверьте сопутствующий код в нашем репозитории GitHub .