Recursos do Java 8 de Mockito

Recursos do Java 8 de Mockito

1. Visão geral

O Java 8 introduziu uma variedade de recursos novos e impressionantes, como lambda e streams. E, naturalmente, Mockito aproveitou essas inovações recentes em seus2nd major version.

Neste artigo, exploraremos tudo o que essa poderosa combinação tem a oferecer.

2. Interface de simulação com um método padrão

A partir do Java 8, agora podemos escrever implementações de métodos em nossas interfaces. Essa pode ser uma ótima funcionalidade nova, mas sua introdução à linguagem violou um conceito forte que fazia parte do Java desde a sua concepção.

A versão 1 do Mockito não estava pronta para essa alteração. Basicamente, porque não nos permitiu pedir para chamar métodos reais de interfaces.

Imagine que temos uma interface com 2 declarações de método: a primeira é a assinatura de método antiquada com a qual estamos todos acostumados e a outra é um métododefault totalmente novo:

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;
        }
    }
}

Observe que o métodoassignJobPosition()default tem uma chamada para o métodofindCurrentJobPosition() não implementado.

Agora, suponha que desejamos testar nossa implementação deassignJobPosition() sem escrever uma implementação real defindCurrentJobPosition(). Poderíamos simplesmente criar uma versão simulada deJobService, e dizer ao Mockito para retornar um valor conhecido da chamada para nosso método não implementado e chamar o método real quandoassignJobPosition() for chamado:

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()));
    }
}

Isso é perfeitamente razoável e funcionaria perfeitamente, já que estávamos usando uma classe abstrata em vez de uma interface.

No entanto, o funcionamento interno do Mockito 1 simplesmente não estava pronto para essa estrutura. Se rodássemos esse código com o Mockito pré versão 2, obteríamos este erro bem descrito:

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 está fazendo seu trabalho e nos dizendo que não pode chamar métodos reais em interfaces, uma vez que essa operação era impensável antes do Java 8.

A boa notícia é que, apenas alterando a versão do Mockito que estamos usando, podemos eliminar esse erro. Usando o Maven, por exemplo, poderíamos usar a versão 2.7.5 (a versão mais recente do Mockito pode ser encontradahere):


    org.mockito
    mockito-core
    2.7.5
    test

Não há necessidade de fazer alterações no código. Na próxima vez que executarmos o teste, o erro não ocorrerá mais.

3. Retornar valores padrão paraOptional eStream

OptionaleStream são outras novas adições do Java 8. Uma semelhança entre as duas classes é que ambas têm um tipo especial de valor que representa um objeto vazio. Este objeto vazio torna mais fácil evitar o até agora onipresenteNullPointerException.

3.1. Exemplo comOptional

Considere um serviço que injetaJobService descrito na seção anterior e tem um método que chamaJobService#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();
    }
}

Agora, suponha que desejemos criar um teste para verificar se, quando uma pessoa não tem um cargo atual, ela tem direito ao subsídio de desemprego.

Nesse caso, forçaríamosfindCurrentJobPosition() a retornar umOptional vazio. Before Mockito 2, fomos obrigados a simular a chamada para esse método:

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));
    }
}

Esta instruçãowhen(…).thenReturn(…) na linha 13 é necessária porque o valor de retorno padrão do Mockito para qualquer chamada de método para um objeto simulado énull. A versão 2 mudou esse comportamento.

Uma vez que raramente lidamos com valores nulos ao lidar comOptional,Mockito now returns an empty Optional by default. Esse é exatamente o mesmo valor que o retorno de uma chamada paraOptional.empty().

Então,when using Mockito version 2, poderíamos nos livrar da linha 13 e nosso teste ainda seria bem-sucedido:

public class UnemploymentServiceImplUnitTest {

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

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

3.2. Exemplo comStream

O mesmo comportamento ocorre quando simulamos um método que retorna umStream.

Vamos adicionar um novo método à nossa interfaceJobService que retorna um Stream que representa todos os cargos em que uma pessoa já trabalhou:

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

Este método é usado em outro novo método que consultará se uma pessoa já trabalhou em um trabalho que corresponde a uma determinada sequência de pesquisa:

public class UnemploymentServiceImpl implements UnemploymentService {

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

Então, suponha que queremos testar adequadamente a implementação desearchJob(), sem ter que nos preocupar em escreverlistJobs()e queremos testar o cenário quando a pessoa ainda não trabalhou em nenhum emprego. Nesse caso, gostaríamos quelistJobs() retornasse umStream vazio.

Before Mockito 2, we would need to mock the call to listJobs() para escrever tal teste:

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, poderíamos abandonar a chamadawhen(…).thenReturn(…), porque agoraMockito 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. Aproveitando as expressões lambda

Com as expressões lambda do Java 8, podemos tornar as instruções muito mais compactas e fáceis de ler. Ao trabalhar com o Mockito, 2 exemplos muito bons da simplicidade trazida pelas expressões lambda sãoArgumentMatcherse customAnswers.

4.1. Combinação de Lambda eArgumentMatcher

Antes do Java 8, precisávamos criar uma classe que implementasseArgumentMatcher e escrever nossa regra personalizada no métodomatches().

Com o Java 8, podemos substituir a classe interna por uma expressão lambda simples:

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. Combinação de Lambda eAnswer personalizado

O mesmo efeito pode ser alcançado ao combinar expressões lambda comAnswer do Mockito.

Por exemplo, se quisermos simular chamadas para o métodolistJobs() para fazê-lo retornar umStream contendo um únicoJobPosition se o nome dePerson for “Peter ”, E umStream vazio, caso contrário, teríamos que criar uma classe (anônima ou interna) que implementasse a interfaceAnswer.

Novamente, o uso de uma expressão lambda, permite escrever todo o comportamento simulado em linha:

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")));
    }
}

Observe que, na implementação acima, não há necessidade da classe internaPersonAnswer.

5. Conclusão

Neste artigo, abordamos como alavancar os novos recursos Java 8 e Mockito 2 juntos para escrever um código mais limpo, mais simples e mais curto. Se você não estiver familiarizado com alguns dos recursos do Java 8 que vimos aqui, consulte alguns de nossos artigos:

Além disso, verifique o código que o acompanha em nossoGitHub repository.