Técnicas Avançadas de Stubbing de Funções
Ao final desta aula, seremos capazes de:
- Retornar sempre o mesmo objeto todas as vezes em que a função especificada for invocada.
- Saber como retornar objetos diferentes em invocações subsequentes de uma função.
- Lançar uma exceção quando a função especificada for invocada.
- Compreender como especificar uma ação customizada (bloco lambda) que é executada quando uma função configurada (stubbed) é invocada.
Esta aula assume que:
Introdução ao Sistema Sob Teste
Durante esta aula, faremos o stubbing das funções de um repositório que fornece operações de CRUD para objetos Task. Para manter o foco didático o mais simples e direto possível, adicionamos as declarações dos componentes do nosso modelo de dados em um único arquivo de código-fonte em Kotlin 2.4.21 chamado Task.kt. Esse arquivo possui a seguinte estrutura:
|
|
A interface TaskRepository possui duas funções das quais faremos o stubbing ao longo desta aula:
- A função
findById()retorna as informações de uma única tarefa utilizando o seu identificador (id) como critério de busca. Se nenhuma tarefa for encontrada no banco de dados, essa função retornanull. - A função
create()insere uma nova tarefa no banco de dados e retorna as informações da tarefa recém-criada.
O código-fonte da interface TaskRepository é apresentado a seguir:
|
|
A seguir, descobriremos como podemos fazer o stubbing dessas funções utilizando o MockK 1.14 em conjunto com o JUnit Jupiter 6.0.3.
Fazendo o Stubbing de Funções com o MockK
Quando fazemos o stubbing de uma função com o MockK, devemos seguir estes três passos fundamentais:
- Invocar a função
every(oucoEveryno caso de suspend functions em Coroutines). - Especificar a função que sofrerá o stubbing e os parâmetros de função esperados.
- Configurar a resposta da invocação da função.
Leitura Adicional:
Vamos analisar quatro exemplos práticos que demonstram como podemos configurar o comportamento de funções com o MockK.
Exemplo 1: Retornando o Mesmo Objeto em Todas as Invocações da Função
Se quisermos retornar o mesmo objeto todas as vezes em que a função especificada for invocada utilizando os parâmetros esperados, devemos configurar o objeto retornado utilizando a função infixa returns(). Essa função recebe o objeto que deve ser retornado como parâmetro.
Vamos garantir que a função findById() da interface TaskRepository retorne uma tarefa aberta (TaskStatus.OPEN) quando for invocada utilizando o parâmetro de função configurado na constante TASK_ID (1L, criada pelo criador de identificador 5L). Após fazermos o stubbing dessa função, o código-fonte da nossa classe de teste fica da seguinte forma:
|
|
Leitura Adicional:
Exemplo 2: Retornando Objetos Diferentes em Invocações Subsequentes da Função
Se quisermos retornar objetos diferentes em invocações subsequentes de uma mesma função, devemos configurar os objetos retornados utilizando a função returnsMany(), que recebe um objeto List<T> (ou múltiplos argumentos via vararg) como parâmetro. Essa lista contém a sequência de objetos que serão retornados.
Em outras palavras, quando a função configurada é invocada pela primeira vez, o MockK retorna o primeiro item da lista. As invocações subsequentes da função sempre retornam o próximo item da lista na ordem definida. Além disso, se os itens da lista se esgotarem em novas chamadas, o MockK continuará retornando o último item da lista.
Vamos garantir que a função findById() da interface TaskRepository retorne dois objetos Task diferentes quando for invocada utilizando o parâmetro TASK_ID. A primeira invocação retorna uma tarefa aberta (TaskStatus.OPEN) e a segunda invocação retorna uma tarefa que foi fechada (TaskStatus.CLOSED) porque foi concluída (TaskResolution.DONE).
Após fazermos o stubbing dessa função, o código-fonte da nossa classe de teste fica assim:
|
|
Leitura Adicional:
Exemplo 3: Lançando uma Exceção
Se quisermos lançar uma exceção quando a função especificada for invocada utilizando os parâmetros esperados, devemos configurar a exceção lançada utilizando a função throws(). Essa função recebe a instância do objeto de exceção a ser lançado como parâmetro.
Vamos garantir que a função findById() da interface TaskRepository lance uma RuntimeException quando for invocada utilizando o parâmetro TASK_ID. Após fazermos o stubbing dessa função, o código-fonte da nossa classe de teste terá a seguinte estrutura:
|
|
Nota Importante:
Quando utilizamos essa técnica, jamais devemos quebrar o contrato da API da função que está sofrendo o stubbing (por exemplo, lançando exceções que a implementação real nunca lançaria naquele contexto). Isso é fundamental porque queremos garantir que o sistema sob teste interaja com as suas dependências de maneira realista e fiel ao comportamento de produção.
Leitura Adicional:
Exemplo 4: Configurando a Ação Executada
Se quisermos configurar dinamicamente a ação que é executada quando a função especificada é invocada com os parâmetros esperados, devemos utilizar a função answers(). Quando invocamos essa função, definimos a ação executada por meio de uma expressão lambda que roda no contexto de um MockKAnswerScope. Além disso, se a função configurada possuir um tipo de retorno, nossa expressão lambda também deverá produzir e retornar o objeto correspondente.
Vamos garantir que a função create() da interface TaskRepository atribua o id da tarefa criada antes de retornar um novo objeto Task contendo os dados recebidos por parâmetro. Dentro do bloco answers, podemos inspecionar o primeiro argumento passado para create() por meio da função auxiliar this.firstArg<Task>() (ou ainda usar input.copy(id = 1L) do Kotlin). Após fazermos o stubbing dessa função, o código-fonte da nossa classe de teste fica assim:
|
|
Leitura Adicional:
Resumo
Esta aula nos ensinou cinco conceitos essenciais sobre stubbing com o MockK:
- Quando queremos fazer o stubbing de uma função com o MockK, devemos iniciar a configuração utilizando a função
every(). - Se quisermos retornar sempre o mesmo objeto todas as vezes em que a função especificada for invocada utilizando os parâmetros esperados, devemos configurar o objeto retornado com a função
returns(). - Se quisermos retornar objetos diferentes em invocações subsequentes da função, devemos configurar a lista de objetos retornados utilizando a função
returnsMany(). - Se quisermos lançar uma exceção quando a função especificada for invocada com os parâmetros esperados, devemos configurar a exceção lançada utilizando a função
throws(). - Se quisermos configurar uma ação customizada que é executada dinamicamente quando a função especificada é invocada com os parâmetros esperados, devemos utilizar a função
answers().