💧🔗 Elixir: Testando chamadas de uma API externa
Tags: elixir, testing, braziliandevs, api
Published: 2022-05-21 00:47:23

Neste post vamos criar testes automatizados para as chamadas da API externa que fizemos na nossa aplicação do post [Elixir: Consumindo dados de uma API externa](https://dev.to/maiquitome/elixir-consumindo-dados-de-uma-api-externa-3g78).

* [Como testar uma chamada de API Externa?](#como-testar)
 - [Tesla.Mock e Mox (simulações estáticas)](#tesla-mock)
 - [Desafio: Trocar Mox por Hammox](#hammox)
 - [ExVCR (gravar e reproduzir as interações HTTP)](#exvcr)
 - [Bypass (servidor HTTP falso)](#bypass)
* [Criando os testes de client com Bypass](#criando-os-testes-de-client)
 - [Quando o CEP é válido](#cep-valido)
 - [Quando o CEP não existe](#cep-nao-existe)
 - [Quando o CEP é inválido](#cep-invalido)
 - [Quando há um erro genérico](#erro-genérico)
* [Criando os testes na regra de negócio com Mox](#criando-os-testes-de-negocio)
 - [Behaviour para a função get_cep_info()](#behaviour)
 - [Criando um Mock para implementar o behaviour](#config-mox)
 - [Client para produção e client para teste (Injeção de Dependência)](#client-config)
 - [Criando os testes de criação de usuário](#criacao-de-usuario)
* [Conclusão](#conclusao)

[![A Jornada do Autodidata em Inglês](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pkoqkmh9hic17tnddjf6.png)](https://go.hotmart.com/R93865620U)

# Como testar uma chamada de API Externa? <a name="como-testar">

Vamos supor que a API externa calculasse o valor do frete de um produto e que nos deixasse realizar apenas 10 requisições/chamadas por minuto. Se fossemos realizar requisições de verdade nos testes, estaríamos diminuindo rápido a quantidade de requisições permitidas e poderíamos deixar o cliente esperando para receber esse calculo; isso poderia prejudicar a venda.

Para conseguirmos realizar os testes sem consumir a quantidade de requisições permitidas, precisamos simular as chamadas da API externa.

Criar uma versão falsa de um serviço externo ou interno que pode substituir o real, ajudando seus testes a serem executados de forma rápida e confiável é denominado de __mock__.

# Tesla.Mock e Mox (simulações estáticas)<a name="tesla-mock">

Existem várias abordagens para se testar chamadas de uma API externa. O próprio __Tesla__ fornece um adapter chamado [Tesla.Mock](https://github.com/elixir-tesla/tesla#testing):

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4edx876f4bjrfwhnjoc8.png)

Outra biblioteca que segue esta mesma abordagem é a [Mox](https://github.com/dashbitco/mox). O José Valim, criador da linguagem Elixir, comenta sobre ela no post [Mocks and explicit contracts](http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/).

O diferencial da __Mox__ é que ela tem uma abordagem de criar contratos explícitos. Conforme José Valim, definir contratos nos permite ver a complexidade em nossas dependências, sendo que a nossa aplicação sempre terá complexidade, portanto, precisamos sempre deixar o mais explícito possível. 

Vamos entender melhor sobre contratos e injeção de dependência mais pra frente quando fizermos os testes usando a __Mox__.
 
Com o `Tesla.Mock` ou com o `Mox`, teríamos um exemplo da `chamada` e da `resposta`, porém; de forma estática. A chamada não acontece de fato, nenhum middleware nesse caso seria executado e não conseguiríamos passar por todo o fluxo. Essa forma de testar pode esconder muitos comportamentos.

Assim, para testar o `client` vamos usar outra abordagem, o de `servidor HTTP falso` que ainda iremos conhecer mas, para os testes da criação do usuário, podemos usar a __Mox__.


Para instalar a Mox adicione ao seu arquivo `mix.exs`:

```elixir
def deps do
  [
    {:mox, "~> 1.0", only: :test}
  ]
end
```

# Desafio: Trocar Mox por Hammox <a name="hammox">

Após finalizarmos a criação dos testes com a lib __Mox__ tente migrar os mesmos para a lib __Hammox__ que garante que suas simulações (mocks) e implementações cumpram o mesmo contrato.

Ele leva a Mox e sua filosofia ao limite, fornecendo testes automáticos de contrato com base em __especificações de tipo de comportamento__, mantendo total compatibilidade com o código que já usa Mox.

O __Hammox__ visa capturar o maior número possível de bugs de contrato, ao mesmo tempo em que fornece rastreamentos profundos úteis para que possam ser facilmente rastreados e corrigidos.

Quando você estiver confortável com a lib __Mox__, acesse https://github.com/msz/hammox e faça a migração.


# ExVCR (gravar e reproduzir as interações HTTP) <a name="exvcr">

Existe outra abordagem chamada __VCR (Videocassete)__. Essa abordagem faz uma chamada real na API externa apenas uma vez e grava a resposta em um arquivo json para depois ser utilizada nos próximos testes sem precisar fazer novamente a chamada na API externa. Existe uma biblioteca para isso em Elixir chamada [ExVCR](https://github.com/parroty/exvcr).

O ponto negativo é que a conexão também não é testada.

# Bypass (servidor HTTP falso) <a name="bypass">

Como queremos ver todo o fluxo acontecendo nos testes de client, as chamadas sendo executadas com os middlewares e o json sendo transformado; podemos usar um **servidor HTTP falso**. Existe uma biblioteca para isso chamada [Bypass](https://github.com/PSPDFKit-labs/bypass).

O `Bypass` sobe um servidor local nos permitindo manipular a conexão. Ao invés de realizarmos chamadas para um servidor na ‘internet’, vamos realizar chamadas para um servidor local; que não precisa de ‘internet’. Desta forma, conseguimos testar realmente o comportamento do nosso `client`.

Essa é a abordagem que iremos usar nos nossos testes de client.

Para instalar o Bypass adicione ao seu arquivo `mix.exs`:

```elixir
def deps do
  [
    {:bypass, "~> 2.1", only: :test}
  ]
end
```


# Criando os testes de Client com Bypass <a name="criando-os-testes-de-client">

Como o `TeslaClient` e o `HttpoisonClient` que criamos no primeiro post tem o mesmo comportamento, iremos criar apenas um arquivo de teste. Na verdade, a única diferença será no erro genérico, mas que vamos arrumar depois. Por Em uma aplicação real você teria apenas um `lib/my_app/via_cep/client.ex`.

Então seguindo essa estrutura teremos o arquivo de teste em `test/my_app/via_cep/client_test.exs`.

No `setup` configuramos que as chamadas não serão realizadas para a API externa, serão realizadas para o servidor local do __Bypass__:

```elixir
defmodule MyApp.ViaCep.ClientTest do
  use ExUnit.Case, async: true

  alias Plug.Conn
  alias MyApp.ViaCep.TeslaClient, as: Client

  describe "get_cep_info/1" do
    setup do
      bypass = Bypass.open()

      {:ok, bypass: bypass}
    end

    test "when the cep is valid", %{bypass: bypass} do
      # vamos implementar depois
    end

    test "when the cep is not found", %{bypass: bypass} do
      # vamos implementar depois
    end

    test "when the cep is invalid", %{bypass: bypass} do
      # vamos implementar depois
    end

    test "when there is a generic error", %{bypass: bypass} 
      # vamos implementar depois
    do
  end

  defp endpoint_url(port), do: "http://localhost:#{port}/"
end
```

Sobre a função `endpoint_url`, ao invés de fazermos chamadas para `https://viacep.com.br/ws/95270000/json/`, nós faremos chamadas para o servidor local do __Bypass__ `https://localhost:porta/ws/95270000/json/`.

A porta sempre irá mudar, pois, como os testes são assíncronos, um servidor diferente subirá para cada chamada e, desta forma, um teste não irá interferir no outro.

Podemos agora partir para a criação dos testes:
1. CEP válido;
2. CEP não existe;
3. CEP inválido;
4. Erro genérico.



# Quando o CEP é válido <a name="cep-valido">

Neste teste vamos utilizar a sigil `~s` para gerar uma string com escape. Você pode obter mais detalhes sobre sigils na [documentação do Elixir](https://elixir-lang.org/getting-started/sigils.html) ou no [Elixir School](https://elixirschool.com/pt/lessons/basics/sigils).

Veja o resultado da sigil `~s`:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/frh6f4jodmw6lf4p6mjk.png) 

Sobre a função `Bypass.expect` e outras similares você pode encontrar na [documentação do Bypass](https://hexdocs.pm/bypass/Bypass.html#expect/4). 

```elixir
...
    test "when the cep is valid", %{bypass: bypass} do
      cep = "01001000"

      url = endpoint_url(bypass.port)

      body = ~s({
        "cep": "01001-000",
        "logradouro": "Praça da Sé",
        "complemento": "lado ímpar",
        "bairro": "Sé",
        "localidade": "São Paulo",
        "uf": "SP",
        "ibge": "3550308",
        "gia": "1004",
        "ddd": "11",
        "siafi": "7107"
      })

      # Bypass.expect(bypass, method, path, fun)
      Bypass.expect(bypass, "GET", "#{cep}/json/", fn conn ->
        conn
        # Coloque o `Conn.put_resp_header()` para executar o middleware do Tesla
        |> Conn.put_resp_header("content-type", "application/json")
        |> Conn.resp(200, body)
      end)

      response = Client.get_cep_info(url, cep)

      expected_response =
        {:ok,
         %{
           "bairro" => "Sé",
           "cep" => "01001-000",
           "complemento" => "lado ímpar",
           "ddd" => "11",
           "gia" => "1004",
           "ibge" => "3550308",
           "localidade" => "São Paulo",
           "logradouro" => "Praça da Sé",
           "siafi" => "7107",
           "uf" => "SP"
         }}

      assert response == expected_response
    end
...
```

Se retirarmos a função `Conn.put_resp_header("content-type", "application/json")` o middleware do Tesla não irá funcionar e não irá transformar o json:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/04aedespasivaoyawf1b.png)
 

# Quando o CEP não existe <a name="cep-nao-existe">

```elixir
...
    test "when the cep is not found", %{bypass: bypass} do
      cep = "00000000"

      body = ~s({"erro": true})

      url = endpoint_url(bypass.port)

      Bypass.expect(bypass, "GET", "#{cep}/json/", fn conn ->
        conn
        |> Conn.put_resp_header("content-type", "application/json")
        |> Conn.resp(200, body)
      end)

      response = Client.get_cep_info(url, cep)

      expected_response = {:ok, %{"erro" => true}}

      assert response == expected_response
    end
...
```


# Quando o CEP é inválido <a name="cep-invalido">


```elixir
...
   test "when the cep is invalid", %{bypass: bypass} do
      cep = "123"

      url = endpoint_url(bypass.port)

      Bypass.expect(bypass, "GET", "#{cep}/json/", fn conn ->
        Conn.resp(conn, 400, "")
      end)

      response = Client.get_cep_info(url, cep)

      expected_response = {:error, %{result: "Invalid CEP!", status: :bad_request}}

      assert response == expected_response
    end
...
```


# Quando há um erro genérico <a name="erro-genérico">

Esse teste não conseguiríamos simular se estivéssemos usando o `Tesla.Mock`. Vamos simular um `timeout`, uma falha de comunicação com o servidor.

```elixir
...
    test "when there is a generic error", %{bypass: bypass} do
      cep = "00000000"

      url = endpoint_url(bypass.port)

      # fechar o servidor somente neste teste
      Bypass.down(bypass)

      response = Client.get_cep_info(url, cep)

      expected_response = {:error, %{result: :econnrefused, status: :bad_request}}

      assert response == expected_response
    end
...
```

Esse é o único comportamento diferente entre os `clients` aqui. Se trocarmos de `TeslaClient` para `HttpoisonClient`:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h7j6tq2keucl0csizjo5.png)

Como a `reason` é `:econnrefused`, poderíamos arrumar isso no arquivo client do HTTPoison e o comportamento ficaria igual em ambos os clients:

```elixir
defp handle_get({:error, %Error{id: _id, reason: reason}}) do
  {:error, %{status: :bad_request, result: reason}}
end
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p98bejcd3i4am0map0sl.png) 
 

# Criando os testes na regra de negócio com Mox <a name="criando-os-testes-de-negocio">

Como comentamos anteriormente, iremos usar a lib __Mox__ para os testes da criação do usuário. Faz mais sentido usarmos essa abordagem nos testes de negócio, pois essa camada está mais acima do client.



# Behaviour para a função get_cep_info() <a name="behaviour">


O __Mox__ trabalha com __behaviours__ para fazer injeção de dependencia nos testes.


Vamos definir um contrato para a função `get_cep_info()`. Quando essa função for implementada ela obrigatoriamente deve receber uma `String` como parâmetro e retornar sempre uma  tupla de `{:ok, map()}` ou `{:error, map()}`.

Em `lib/my_app/via_cep/behaviour.ex`:

```elixir
defmodule MyApp.ViaCep.Behaviour do
  @callback get_cep_info(String.t()) :: {:ok, map()} | {:error, map()}
end
```

Altere os arquivos `httpoison_client` e `tesla_client` adicionando o __behaviour__:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/011ch8uf9fvecofwuek1.png) 


Se tentarmos retornar algo diferente para esta função, receberemos um aviso:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z6jc0ca54s1hxdrf3b0r.png)



# Criando um Mock para implementar o behaviour <a name="config-mox">

Vamos configurar o __Mox__ para criar no teste um mock que também implementa esse behaviour.

Em `test/test_helper.exs`:

```elixir
ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, :manual)

# adicione essa linha
Mox.defmock(MyApp.ViaCep.ClientMock, for: MyApp.ViaCep.Behaviour)
```


# Client para produção e client para teste (Injeção de Dependência) <a name="client-config">

Vamos injetar através do __ambiente que estamos executando (dev, prod ou test)__ o Client que iremos utilizar.

Vamos criar uma configuração pra quando você estiver em ambiente de teste ser usado o mock `MyApp.ViaCep.ClientMock`, e quando estiver em um ambiente de dev ou produção ser usado a implementação `MyApp.ViaCep.TeslaClient`.


Em `config/config.exs`:

```elixir
...
config :my_app,
  ecto_repos: [MyApp.Repo]

# adicione
config :my_app,
       MyApp.Users.Create,
       via_cep_adapter: MyApp.ViaCep.TeslaClient
...
```

Em `config/config.exs` os valores valem para qualquer ambiente e, se exister em outro ambiente com a mesma configuração, o valor é subscrito. 

Testando com `iex -S mix`:
```elixir
iex> Application.fetch_env!(:my_app, MyApp.Users.Create)[:via_cep_adapter]         
MyApp.ViaCep.TeslaClient
```

* `fetch_env` roda em tempo de execução.
* `compile_env` em tempo de compilação.

Em `config/test.exs`:

```elixir
...
config :my_app, MyApp.Repo,
  username: "postgres",
  password: "postgres",
  hostname: "localhost",
  database: "my_app_test#{System.get_env("MIX_TEST_PARTITION")}",
  pool: Ecto.Adapters.SQL.Sandbox,
  pool_size: 10

# adicione
# ClientMock ao invés de Client
config :my_app,
       MyApp.Users.Create,
       via_cep_adapter: MyApp.ViaCep.ClientMock
...
```

Testando com `MIX_ENV=test iex -S mix`:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/10gkj1axhf2lpveuk9wt.png)
 

Em `lib/my_app/users/create.ex`:

```elixir
  defp client do
    # assim:
    # Application.fetch_env!(:my_app, __MODULE__)[:via_cep_adapter]

    # ou:
    :my_app
    |> Application.fetch_env!(__MODULE__)
    |> Keyword.get(:via_cep_adapter)
  end
```

Troque `Client.get_cep_info(cep)` para `client().get_cep_info(cep)`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j6cq8p3ihwpgdu06iu0i.png) 


# Criando os testes de criação de usuário <a name="criacao-de-usuario">

Em `test/my_app/users/create_test.exs`:

```elixir
defmodule MyApp.Users.CreateTest do
  use MyApp.DataCase, async: true

  import Mox

  alias MyApp.{User, Users.Create, ViaCep.ClientMock}

  describe "call/1" do
    test "when all params are valid" do
      params = %{
        first_name: "Mike",
        last_name: "Wazowski",
        cep: "95270000",
        email: "mike_wazowski@monstros_sa.com"
      }

      expect(ClientMock, :get_cep_info, fn _cep ->
        {:ok,
         %{
           "bairro" => "Sé",
           "cep" => "01001-000",
           "complemento" => "lado ímpar",
           "ddd" => "11",
           "gia" => "1004",
           "ibge" => "3550308",
           "localidade" => "São Paulo",
           "logradouro" => "Praça da Sé",
           "siafi" => "7107",
           "uf" => "SP"
         }}
      end)

      response = Create.call(params)

      assert {:ok, %User{id: _id, email: "mike_wazowski@monstros_sa.com"}} = response
    end

    test "when there are invalid params" do
      params = %{
        first_name: "Mike",
        last_name: "Wazowski",
        cep: "123",
        email: "monstros_sa.com"
      }

      response = Create.call(params)

      expected_response = %{
        cep: ["should be 8 character(s)"],
        email: ["has invalid format"]
      }

      assert {:error, changeset} = response

      assert errors_on(changeset) == expected_response
    end
  end
end
```

Se trocarmos o `_cep` por `00000000` receberemos um erro:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/frdh4ijs5zkpu5lgy4hg.png)

 
No segundo teste `"when there are invalid params"`, não precisemos usar o mock pois a função `get_cep_info()` não é acionada. O retorno acontece na validação da função `User.validate_before_insert(changeset)`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x9idg03eeha1jde2jmok.png)


# Conclusão <a name="conclusao">


Quando falarmos em testes de client podemos pensar logo em usarmos a abordagem de um __Servidor HTTP Falso__ usando a lib __Bypass__ para testarmos todo o fluxo e todos os comportamentos de uma chamada externa. Já quando falarmos em camadas mais acima, camadas de regra de negócio, podemos criar contratos explícitos definindo o comportamento de funções usando uma lib como a __Mox__ ou __Hammox__.