Статьи

Более компактный Mockito с Java 8 и лямбда-выражениями

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

В начале 2015 года я выступил с докладом по флеш-памяти. Java 8 дает возможность тестировать! на GeeCON TDD 2015 и DevConf.cz 2015. В своей речи на 4 примерах я показал, как Java 8, а именно лямбда-выражения, может упростить инструменты тестирования и тестирования в целом. Одним из таких инструментов был Mokcito. Чтобы не позволить моему PoC-коду умереть на слайдах и сделать его просто доступным для других, я выпустил небольшой проект с двумя полезными в указанном случае дополнениями Java 8 для Mockito.

Быстрое введение

В качестве предпосылки предположим, что у нас есть следующая структура данных:

@Immutable
class ShipSearchCriteria {
    int minimumRange;
    int numberOfPhasers;
}

и класс, который мы хотим заглушить / издеваться:

public class TacticalStation {
  public int findNumberOfShipsInRangeByCriteria(
                               ShipSearchCriteria searchCriteria) { ... }
}

Библиотека предоставляет два дополнения:

Lambda matcher — позволяет определять логику соответствия в лямбда-выражении.

given(ts.findNumberOfShipsInRangeByCriteria(
    argLambda(sc -> sc.getMinimumRange() > 1000))).willReturn(4);

Argument Captor — Java 8 edition — позволяет использовать `ArgumentCaptor` в одну строку (здесь с AssertJ):

verify(ts).findNumberOfShipsInRangeByCriteria(
    assertArg(sc -> assertThat(sc.getMinimumRange()).isLessThan(2000)));

Лямбда-матчер

С помощью статического метода argLambda создается экземпляр лямбда-сопоставителя, который можно использовать для определения логики сопоставления в лямбда-выражении (здесь для заглушки). Это может быть особенно полезно при работе со сложными классами, передаваемыми в качестве аргумента.

@Test
public void shouldAllowToUseLambdaInStubbing() {
    //given
    given(ts.findNumberOfShipsInRangeByCriteria(
        argLambda(sc -> sc.getMinimumRange() > 1000))).willReturn(4);
    //expect
    assertThat(ts.findNumberOfShipsInRangeByCriteria(
        new ShipSearchCriteria(1500, 2))).isEqualTo(4);
    //expect
    assertThat(ts.findNumberOfShipsInRangeByCriteria(
        new ShipSearchCriteria(700, 2))).isEqualTo(0);
}

Для сравнения та же логика, реализованная с пользовательским Ответом в Java 7:

@Test
public void stubbingWithCustomAsnwerShouldBeLonger() {  //old way
    //given
    given(ts.findNumberOfShipsInRangeByCriteria(any())).willAnswer(new Answer<Integer>() {
        @Override
        public Integer answer(InvocationOnMock invocation) throws Throwable {
            Object[] args = invocation.getArguments();
            ShipSearchCriteria criteria = (ShipSearchCriteria) args[0];
            if (criteria.getMinimumRange() > 1000) {
                return 4;
            } else {
                return 0;
            }
        }
    });
    //expect
    assertThat(ts.findNumberOfShipsInRangeByCriteria(
        new ShipSearchCriteria(1500, 2))).isEqualTo(4);
    //expect
    assertThat(ts.findNumberOfShipsInRangeByCriteria(
        new ShipSearchCriteria(700, 2))).isEqualTo(0);
}

Даже Java 8 и менее читаемые конструкции не слишком помогают:

@Test
public void stubbingWithCustomAsnwerShouldBeLongerEvenAsLambda() {  //old way
    //given
    given(ts.findNumberOfShipsInRangeByCriteria(any())).willAnswer(invocation -> {
        ShipSearchCriteria criteria = (ShipSearchCriteria) invocation.getArguments()[0];
        return criteria.getMinimumRange() > 1000 ? 4 : 0;
    });
    //expect
    assertThat(ts.findNumberOfShipsInRangeByCriteria(
        new ShipSearchCriteria(1500, 2))).isEqualTo(4);
    //expect
    assertThat(ts.findNumberOfShipsInRangeByCriteria(
        new ShipSearchCriteria(700, 2))).isEqualTo(0);
}

Аргумент Captor — Java 8 издание

Статический метод assertArg создает сопоставление аргументов, реализация которого внутренне использует ArgumentMatcher с утверждением, предоставленным в лямбда-выражении. В приведенном ниже примере AssertJ предоставляет значимое сообщение об ошибке, но можно использовать любые утверждения (например, native от TestNG или JUnit) (если это действительно необходимо). Это позволяет встроить ArgumentCaptor:

@Test
public void shouldAllowToUseAssertionInLambda() {
    //when
    ts.findNumberOfShipsInRangeByCriteria(searchCriteria);
    //then
    verify(ts).findNumberOfShipsInRangeByCriteria(
        assertArg(sc -> assertThat(sc.getMinimumRange()).isLessThan(2000)));
}

По сравнению с 3 линиями классическим способом:

@Test
public void shouldAllowToUseArgumentCaptorInClassicWay() {  //old way
    //when
    ts.findNumberOfShipsInRangeByCriteria(searchCriteria);
    //then
    ArgumentCaptor<ShipSearchCriteria> captor = 
        ArgumentCaptor.forClass(ShipSearchCriteria.class);
    verify(ts).findNumberOfShipsInRangeByCriteria(captor.capture());
    assertThat(captor.getValue().getMinimumRange()).isLessThan(2000);
}

Резюме

Представленные дополнения были созданы как PoC для моей речи на конференции, но должны быть полностью функциональными и потенциально полезными в конкретных случаях. Чтобы использовать его в своем проекте, достаточно использовать Mockito 1.10.x или 2.0.x-beta, добавить `mockito-java8` в качестве зависимости и, конечно, скомпилировать ваш проект с Java 8+.  

Более подробная информация доступна на веб-странице проекта: https://github.com/szpak/mockito-java8