Возможности Mockito Java 8
1. обзор
Java 8 представила ряд новых, удивительных функций, таких как лямбда и потоки. И, естественно, Mockito использовал эти последние инновации в своих2nd major version.
В этой статье мы рассмотрим все, что может предложить эта мощная комбинация.
2. Мокинг интерфейса с помощью метода по умолчанию
Начиная с Java 8, теперь мы можем писать реализации методов в наших интерфейсах. Это может быть отличной новой функциональностью, но его введение в язык нарушило сильную концепцию, которая была частью Java с момента ее появления.
Mockito версии 1 не был готов к этому изменению. В основном потому, что он не позволял нам запрашивать вызов реальных методов из интерфейсов.
Представьте, что у нас есть интерфейс с двумя объявлениями методов: первое - это устаревшая сигнатура метода, к которой мы все привыкли, а второе - это совершенно новый методdefault:
public interface JobService {
Optional findCurrentJobPosition(Person person);
default boolean assignJobPosition(Person person, JobPosition jobPosition) {
if(!findCurrentJobPosition(person).isPresent()) {
person.setCurrentJobPosition(jobPosition);
return true;
} else {
return false;
}
}
}
Обратите внимание, что методassignJobPosition()default имеет вызов нереализованного метода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 можно найтиhere):
org.mockito
mockito-core
2.7.5
test
Нет необходимости вносить какие-либо изменения в код. При следующем запуске нашего теста ошибка больше не будет возникать.
3. Вернуть значения по умолчанию дляOptional иStream
Optional иStream - другие новые дополнения Java 8. Одно сходство между этими двумя классами состоит в том, что оба имеют специальный тип значения, представляющего пустой объект. Этот пустой объект позволяет избежать до сих пор вездесущегоNullPointerException.
3.1. Пример сOptional
Рассмотрим службу, которая вводит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 optional = jobService.findCurrentJobPosition(person);
return !optional.isPresent();
}
}
Теперь предположим, что мы хотим создать тест для проверки того, что, если у человека нет текущей должности, он имеет право на пособие по безработице.
В этом случае мы бы заставилиfindCurrentJobPosition() вернуть пустойOptional. Before 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 now returns an empty Optional by default. Это то же самое значение, что и возврат вызоваOptional.empty().
Итак,when using Mockito version 2, мы можем избавиться от строки 13, и наш тест все равно будет успешным:
public class UnemploymentServiceImplUnitTest {
@Test
public void givenReturnIsOptional_whenDefaultValueIsReturned_thenValueIsEmpty() {
Person person = new Person();
assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(person));
}
}
3.2. Пример сStream
То же самое происходит, когда мы имитируем метод, который возвращаетStream.
Давайте добавим новый метод в наш интерфейсJobService, который возвращает Stream, представляющий все должности, на которых когда-либо работал человек:
public interface JobService {
Stream listJobs(Person person);
}
Этот метод используется в другом новом методе, который будет запрашивать, работал ли человек когда-либо над заданием, которое соответствует заданной строке поиска:
public class UnemploymentServiceImpl implements UnemploymentService {
@Override
public Optional searchJob(Person person, String searchString) {
return jobService.listJobs(person)
.filter((j) -> j.getTitle().contains(searchString))
.findFirst();
}
}
Итак, предположим, что мы хотим правильно протестировать реализациюsearchJob(),, не беспокоясь о написанииlistJobs(), и предположим, что мы хотим протестировать сценарий, когда человек еще не работал на какой-либо работе. В этом случае мы бы хотели, чтобыlistJobs() возвращал пустойStream.
Before Mockito 2, we would need to mock the call to 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());
}
}
If we upgrade to version 2, мы можем отказаться от вызоваwhen(…).thenReturn(…), потому что теперьMockito will return an empty Stream on mocked methods by default:
public class UnemploymentServiceImplUnitTest {
@Test
public void givenReturnIsStream_whenDefaultValueIsReturned_thenValueIsEmpty() {
Person person = new Person();
assertFalse(unemploymentService.searchJob(person, "").isPresent());
}
}
4. Использование лямбда-выражений
С помощью лямбда-выражений Java 8 мы можем сделать операторы более компактными и удобными для чтения. При работе с Mockito два очень хороших примера простоты, вносимой лямбда-выражениями, - этоArgumentMatchers и пользовательскийAnswers.
4.1. Сочетание лямбды и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. Комбинация лямбды и пользовательскогоAnswer
Тот же эффект может быть достигнут при сочетании лямбда-выражений сAnswer Mockito.
Например, если мы хотим смоделировать вызовы методаlistJobs(), чтобы он возвращалStream, содержащий единственныйJobPosition, если имяPerson - «Питер ”, И пустойStream, иначе нам пришлось бы создать класс (анонимный или внутренний), реализующий интерфейсAnswer.
Опять же, использование лямбда-выражения, позволяет нам записать все макеты поведения в строке:
public class CustomAnswerWithLambdaUnitTest {
@Before
public void init() {
MockitoAnnotations.initMocks(this);
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 repository.