Stubs e Mocks em Java com JUnit
De uns tempos pra cá, passei a trabalhar com alguns projetos Java. Da primeira vez que precisei implementar testes que cobrissem métodos que dependiam de requisições externas, fiquei um pouco perdida sobre qual seria a forma ideal para trabalhar com mocks e stubs no JUnit. Por conta disso, consultei o livro Pragmatic Unit Testing in Java 8 with JUnit. Este post reúne o que aprendi com ele.
Para fins de demonstração, usarei um exemplo que executa uma requisição para a Star Wars API.
A partir do nome de um determinado planeta, o método
getClimate()
, busca as informações referentes ao planeta através da API e
retorna o seu clima.
Criamos um objeto HttpImpl que através do método get()
se encarrega de
passar o endereço de uma URL e retorna as informações fornecidas
pela API. Em seguida, extrai do resultado obtido apenas o dado desejado (o clima
do planeta) e finalmente, o retorna.
Indo mais a fundo, segue o código na íntegra de HttpImpl:
Podemos perceber que HttpImpl é uma implementação que performa uma requisição GET para uma API qualquer e retorna o seu conteúdo.
Tendo conhecimento do código deste projeto e partindo do princípio que HttpImpl
já possui os testes unitários que cobrem o seu funcionamento, como poderíamos
escrever um teste automatizado para o método getClimate()
?
Uma primeira tentativa:
Aqui realizamos a chamada para getClimate()
passando a string Tatooine como
nome do planeta a ser buscado. Então, checamos se o resultado é igual à “arid”,
pois é o valor contido em climate
ao buscar pelo planeta Tatooine fazendo uso
da API.
Problema: Ao rodar a linha String climate = planet.getClimate("Tatooine");
,
este teste realiza uma requisição real para a API. Esta prática é considerada
ruim por 2 motivos:
- Realizar uma chamada HTTP faz com que a execução dos testes se torne mais lenta.
- Não existe garantia de que a API sempre retorne o mesmo valor. Não queremos que mudanças feitas na API levem o teste a falhar.
Uso de stubs
Para evitar a requisição real para a API, especificaremos uma string hardcoded
nos testes. Desta forma, ao chamar o método get()
no objeto HttpImpl, iremos
substituir o seu valor real pelo valor da string especificada. Este tipo de
implementação é chamada de stub.
O stub acima define uma string muito similar ao retorno fornecido pela API, entretanto contém apenas os dados necessários neste contexto (nome do Planeta e clima).
Tendo o stub definido, é necessário que a classe Planet faça uso dele ao invés da real implementação definida em HttpImpl ao ser executada no ambiente de testes.
Uma das formas de fazer isso é através da técnica de injeção de dependência.
Podemos injetar o objeto Http como um parâmetro de um método construtor em Planet e aplicá-lo a um atributo http.
Agora é possível utilizar a injeção de dependência nos testes passando o stub como objeto Http:
Pronto! Temos um teste automatizado que checa a nossa implementação sem performar
requisições externas. Entretanto, ainda pode ser melhorado. É importante ter em
mente que o funcionamento de getClimate()
depende da passagem de um parâmetro
name, que é acoplado à query string enviada para a API. O nosso teste atual
não cobre esse funcionamento.
Verificação de parâmetros passados para a URL
Podemos adicionar uma condição no início do teste que o força a falhar caso o parâmetro passado para a URL não contenha a informação esperada:
Nosso stub se tornou um pouco mais parecido com uma implementação real do elemento que ele substitui, pois verifica também os parâmetros passados. Perceba, porém, que o teste se tornou bastante verboso. Em um projeto real, diversas implementações similares a essa sendo implementadas manualmente apresentam um esforço de manutenção considerável.
Para facilitar este cenário, podemos utilizar uma ferramenta chamada Mockito.
Mockito
Mockito é um framework de testes unitários utilizado para a criação de mocks.
Um mock pode ser considerado a evolução de um stub. É um objeto que simula o comportamento de um determinado elemento de forma controlada. É possível definir o retorno que queremos que um mock produza em certas condições.
Ferramentas de injeção de dependência do Mockito
O framework Mockito fornece algumas ferramentas de injeção de dependência interessantes que permitem refatorar os testes, de forma a torná-los mais legíveis. Facilita também a verificação de erros, dado que o mock é automaticamente identificado pelo nome do atributo.
Este refactor também permite que o construtor de Planet seja removido (lembre que este construtor nunca foi necessário para o funcionamento de Planet, havia sido adicionado exclusivamente por conta dos testes):
Como o mocking agora está sendo feito no nível dos atributos, o construtor não é mais necessário.
Para ver mais
- Todos os exemplos utilizados neste post estão contidos neste repositório.
- A base para estes exemplos foi retirada do livro Pragmatic Unit Testing in Java 8 with JUnit.