Configurando Respostas Sequenciais, Computadas e Exceções
Depois de concluirmos esta aula, nós:
- Saberemos como retornar o mesmo objeto ou valor em todas as invocações de um método stubbado (
returns). - Saberemos como retornar objetos diferentes em invocações sucessivas (
returnsMany). - Saberemos como determinar dinamicamente o objeto retornado inspecionando os parâmetros recebidos pelo método stubbado (
answers { ... }). - Compreenderemos como modificar o próprio objeto recebido como parâmetro antes de retorná-lo (como atribuir um
idao salvar uma entidade). - Saberemos como fazer o método stubbado lançar uma exceção (
throws).
1. Conhecendo as Mudanças no Sistema Sob Especificação
Nesta aula, continuaremos utilizando o stub de MessageRepository para especificar o comportamento da classe MessageService. Porém, fizemos duas adições importantes aos nossos componentes:
Primeiro, adicionamos o método save(message: Message): Message à interface MessageRepository:
|
|
Segundo, adicionamos o método create(message: Message): Message à classe MessageService, que repassa a mensagem para repository.save(message) e retorna a mensagem salva:
|
|
Vamos agora explorar seis exemplos práticos que demonstram todas as formas de configurar a resposta de um método stubbado com MockK e Kotest!
2. Configurando a Resposta de um Método Stubbado
2.1. Retornando o Mesmo Objeto (ou Valor) em Todas as Invocações (returns)
Quando queremos retornar exatamente o mesmo objeto ou valor toda vez que o método stubbado for chamado (equivalente ao operador >> do Spock), usamos a função infixa returns do MockK:
|
|
2.2. Retornando Objetos Diferentes em Invocações Sucessivas (returnsMany)
Quando queremos que o método stubbado retorne valores diferentes na primeira, segunda, terceira invocação e assim por diante (equivalente ao operador >>> do Spock), utilizamos a função infixa returnsMany passando uma lista de valores (ou encadeando .andThen(...)):
|
|
Como podemos ver, na primeira chamada findById(1L) retorna Optional.of(first), e na segunda chamada retorna Optional.of(second).
[!NOTE] Use respostas sequenciais com parcimônia! Na maioria dos casos, testes determinísticos devem fazer uma única chamada ao sistema sob teste. Entretanto,
returnsManyé muito útil quando testamos algoritmos de retry ou paginação que invocam o mesmo colaborador várias vezes dentro de uma única operação.
2.3. Determinando o Objeto Retornado a Partir do Parâmetro do Método (answers)
Se quisermos decidir qual objeto retornar inspecionando o parâmetro passado ao método stubbado (equivalente ao >> { args -> ... } ou >> { Long id -> ... } do Spock), utilizamos a função answers { ... } do MockK.
Dentro da lambda de answers, o MockK fornece acesso aos argumentos da chamada de duas maneiras equivalentes às duas opções do Spock:
Opção 1 (Lista de argumentos args): podemos acessar a lista de todos os argumentos passados através da propriedade args (onde args[0] é o primeiro argumento):
|
|
Opção 2 (Helper tipado firstArg<Long>() — Recomendado!): em vez de fazer cast manual em args[0], o MockK oferece as funções tipadas firstArg<T>(), secondArg<T>(), thirdArg<T>() e lastArg<T>():
|
|
[!TIP] A segunda opção (
firstArg<Long?>()) é muito mais limpa, segura em relação a tipos e idiomática em Kotlin do que indexarargs[0].
2.4. Construindo o Objeto Retornado Dentro de uma Lambda (answers)
Também podemos usar answers { ... } quando queremos instanciar e configurar o objeto retornado sob demanda a cada invocação do método stubbado:
|
|
2.5. Modificando o Parâmetro do Método Antes de Retorná-lo
Um caso de uso extremamente comum ao testar serviços que salvam entidades no banco de dados é simular a geração da chave primária (id): o método repository.save(message) recebe um objeto Message com id = null, atribui id = 1L nesse objeto e o retorna.
Com answers e firstArg<Message>(), podemos fazer exatamente isso:
|
|
2.6. Lançando uma Exceção (throws)
Se quisermos garantir que o método stubbado lance uma exceção quando for invocado (equivalente ao >> { throw new ... } do Spock), utilizamos a função infixa throws do MockK:
|
|
[!WARNING] Cuidado para não violar o contrato da API ao configurar um Stub!
Pelo contrato real da nossa interface
MessageRepository, o métodofindById()não lançaNotFoundExceptionquando nenhuma mensagem é encontrada — ele retornaOptional.empty(), e quem lançaNotFoundExceptioné oMessageService!Usamos o exemplo acima exclusivamente para demonstrar a sintaxe de
throws. Em seus testes reais, nunca configure um stub para retornar valores ou lançar exceções que contradigam o contrato real do colaborador, pois isso gerará testes falsos-positivos que passam no build mas falham em produção!
Resumo
Nesta aula, aprendemos quatro técnicas essenciais para configurar o retorno de stubs com MockK no Kotest:
- Retornamos um valor fixo em todas as invocações usando
every { ... } returns valor(e valores sequenciais usandoreturnsMany). - Determinamos o retorno dinamicamente com base nos argumentos recebidos usando
answers { firstArg<T>() ... }. - Podemos modificar o argumento recebido e devolvê-lo dentro de
answers { ... }. - Fazemos o método stubbado lançar uma exceção usando
every { ... } throws MinhaException(...).