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.