[Elixir] ExUnit: Asserções e mocks para chamadas de funções

Neste post, vou mostrar maneiras de criar mocks para testes automatizados utilizando Elixir. E também testes que verificam se uma função foi chamada ou não dentro de um fluxo.

Caso você ainda não conheça o conceito de "mock" ou "mockar" aqui vai uma breve descrição:

Quando dizemos que vamos realizar um mock, significa que criaremos uma função ou módulo que só existirá dentro do escopo do teste, e servirá para substituir as funções ou módulos que estão presentes na aplicação, nos retornando resultados específicos que definimos no próprio teste.

1. Utilizando a biblioteca Mock

Essa biblioteca nos trás diversas funções úteis para criar mocks em testes como:

with_mock/2: Recebe um módulo e um keyword list contendo o nome da função e o comportamento a ser definido.

with_mocks/3: Recebe uma lista de keyword lists semelhantes ao with_mock/2.

called/1: Recebe uma função de um módulo que foi mockado com with_mock/2 ou with_mocks/3 e verifica se ela foi chamada dentro do fluxo de código que está sendo testado

Vamos aos exemplos

Para isso, devemos ter um módulo responsável pelas consultas. Vamos criar um e nomea-lo de ExampleClient.

Agora adicione a função get_example_data/0, que retorna uma tupla {:ok, %{data: "hello”}}. (Não vamos requisitar uma API de verdade, o exemplo é apenas didático).

Com nosso módulo de consultas pronto, vamos disponibilizar o seu retorno em uma rota, através de um controller e uma view. Veja como ficaram os arquivos:

/lib/mock_example_web/controllers/example_controller.ex

/lib/mock_example_web/view/example_view.ex

/lib/mock_example_web/router.ex

Feito as alterações, o nosso endpoint já deve estar funcionando.

Testando o nosso ExampleClient

Vamos garantir que nosso endpoint sempre retorne status 200 com o body “%{“data” => %{“data” => “hello”}}”, para isso devemos escrever o seguinte teste:

/test/mock_example_web/controllers/example_controller_test.exs

Criando o nosso Mock

Dessa forma, sempre que o teste passar por um MockExample.ExampleClient.get_example_data/0 no fluxo testado, a bibliotexa irá substituir o modulo da implementação pelo nosso mock definido no teste com o with_mock/2

E se quisermos garantir que a nossa função será sempre chamada no fluxo do teste?

Veja o que acontece ao rodarmos os testes, caso deixarmos de chamar a função “MockExample.ExampleCliente.get_example_data()” no corpo do nosso controller:

Os (grandes) problemas da biblioteca “Mock”

  • Ao utilizarmos o with mock/2 ou with_mocks/2, todo o módulo da implementação é substituido pelo mock criado. Outras funções do módulo definidas no código da implementação deixarão de existir no escopo do teste. Caso o módulo que foi mockado tenha várias funções diferentes sendo chamadas no fluxo da implementação, teremos que definir um novo comportamento no código do teste para cada uma delas:
  • O design força o alto acoplamento dos mocks no teste com o código de implementação. Isso significa que, caso tivermos que mudar o módulo ExampleClient para um outro, OtherExampleClient por exemplo, vários testes podem quebrar, e teremos que alterar o nosso código de teste, substituindo todos os mocks. Seria muito mais conveniente se houvesse uma maneira de trocar em um lugar só, não é mesmo?
  • Não conseguiremos rodar os testes de maneira assíncrona, dado que a biblioteca altera o estado global da aplicação. Convenhamos que isso é péssimo.

2. Utilizando um parâmetro “opts”

Veja os exemplos:

Supondo que temos um client “MyClient” e o utilizamos em outro módulo denominado “Teste”.

/lib/mockss/my_client.exs

/lib/mockss/teste.exs

Utilizando o opts e Keyword.get/3 conseguimos injetar qualquer Mock quando necessário, além disso, setamos o valor padrão para quando não estivermos utilizando mocks, veja como no exemplo:

/lib/mockss/teste.exs

Também conseguimos testar que uma determinada função foi realmente chamada dentro do fluxo (exemplo no terceiro teste):

/test/mockss/teste_test.exs

Particularmente, essa é a minha alternativa preferida para mocks.

3. Separação por ambientes

  • Teste
  • Produção
  • Desenvolvimento

Ao invés de ficarmos criando diversos mocks dentro de testes, devemos criar um módulo específico para cada ambiente, de acordo com a sua necessidade:

  • ExampleClient.Homologation, responsável pelo comportamento no ambiente de desenvolvimento.
  • ExampleClient.Test, responsável pelo comportamento no ambiente de testes.
  • ExampleClient, responsável pelo comportamento no ambiente de produção

Para utilizar os três módulos, basta defini-los na configuração da aplicação:

E onde forem chamados, separamos em um atributo de módulo:

Dessa forma, de acordo com o ambiente que o Elixir estiver rodando, teremos um módulo “diferente”. No caso do ambiente de teste, teremos ExampleClientTest.

Rodando em ambiente de desenvolvimento com mix phx.server:

Rodando em ambiente de teste:

/test/mock_example_web/controllers/example_controller_test.exs:

Dado que Elixir têm o conceito de Pattern Matching, fica bem fácil e simples definirmos um comportamento específico Mockado para os diferentes tipos de chamadas e testes que a mesma função pode receber =)

@truehenrique

/truehenrique.com/ desenvolvimento de software, meu dia a dia e uma “pitada” de filosofia.

@truehenrique

https://www.truehenrique.com/

Henrique F. Teixeira

Written by

Analista de sistemas na @rdstation e pseudo-escritor! (https://truehenrique.com)

@truehenrique

https://www.truehenrique.com/