Возможности Mockito для Java 8

Возможности 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.