Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
May 21, 2022 12:47 am GMT

Elixir: Testando chamadas de uma API externa

Neste post vamos criar testes automatizados para as chamadas da API externa que fizemos na nossa aplicao do post Elixir: Consumindo dados de uma API externa.

  • Como testar uma chamada de API Externa?
    • Tesla.Mock e Mox (simulaes estticas)
    • Desafio: Trocar Mox por Hammox
    • ExVCR (gravar e reproduzir as interaes HTTP)
    • Bypass (servidor HTTP falso)
  • Criando os testes de client com Bypass
    • Quando o CEP vlido
    • Quando o CEP no existe
    • Quando o CEP invlido
    • Quando h um erro genrico
  • Criando os testes na regra de negcio com Mox
    • Behaviour para a funo get_cep_info()
    • Criando um Mock para implementar o behaviour
    • Client para produo e client para teste (Injeo de Dependncia)
    • Criando os testes de criao de usurio
  • Concluso

Como testar uma chamada de API Externa?

Vamos supor que a API externa calculasse o valor do frete de um produto e que nos deixasse realizar apenas 10 requisies/chamadas por minuto. Se fossemos realizar requisies de verdade nos testes, estaramos diminuindo rpido a quantidade de requisies permitidas e poderamos deixar o cliente esperando para receber esse calculo; isso poderia prejudicar a venda.

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

Criar uma verso falsa de um servio externo ou interno que pode substituir o real, ajudando seus testes a serem executados de forma rpida e confivel denominado de mock.

Tesla.Mock e Mox (simulaes estticas)

Existem vrias abordagens para se testar chamadas de uma API externa. O prprio Tesla fornece um adapter chamado Tesla.Mock:

Image description

Outra biblioteca que segue esta mesma abordagem a Mox. O Jos Valim, criador da linguagem Elixir, comenta sobre ela no post Mocks and explicit contracts.

O diferencial da Mox que ela tem uma abordagem de criar contratos explcitos. Conforme Jos Valim, definir contratos nos permite ver a complexidade em nossas dependncias, sendo que a nossa aplicao sempre ter complexidade, portanto, precisamos sempre deixar o mais explcito possvel.

Vamos entender melhor sobre contratos e injeo de dependncia mais pra frente quando fizermos os testes usando a Mox.

Com o Tesla.Mock ou com o Mox, teramos um exemplo da chamada e da resposta, porm; de forma esttica. A chamada no acontece de fato, nenhum middleware nesse caso seria executado e no conseguiramos 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 criao do usurio, podemos usar a Mox.

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

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

Desafio: Trocar Mox por Hammox

Aps finalizarmos a criao dos testes com a lib Mox tente migrar os mesmos para a lib Hammox que garante que suas simulaes (mocks) e implementaes cumpram o mesmo contrato.

Ele leva a Mox e sua filosofia ao limite, fornecendo testes automticos de contrato com base em especificaes de tipo de comportamento, mantendo total compatibilidade com o cdigo que j usa Mox.

O Hammox visa capturar o maior nmero possvel de bugs de contrato, ao mesmo tempo em que fornece rastreamentos profundos teis para que possam ser facilmente rastreados e corrigidos.

Quando voc estiver confortvel com a lib Mox, acesse https://github.com/msz/hammox e faa a migrao.

ExVCR (gravar e reproduzir as interaes HTTP)

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 prximos testes sem precisar fazer novamente a chamada na API externa. Existe uma biblioteca para isso em Elixir chamada ExVCR.

O ponto negativo que a conexo tambm no testada.

Bypass (servidor HTTP falso)

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.

O Bypass sobe um servidor local nos permitindo manipular a conexo. Ao invs de realizarmos chamadas para um servidor na internet, vamos realizar chamadas para um servidor local; que no 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:

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

Criando os testes de Client com Bypass

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 diferena ser no erro genrico, mas que vamos arrumar depois. Por Em uma aplicao real voc teria apenas um lib/my_app/via_cep/client.ex.

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

No setup configuramos que as chamadas no sero realizadas para a API externa, sero realizadas para o servidor local do Bypass:

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 funo endpoint_url, ao invs de fazermos chamadas para https://viacep.com.br/ws/95270000/json/, ns faremos chamadas para o servidor local do Bypass https://localhost:porta/ws/95270000/json/.

A porta sempre ir mudar, pois, como os testes so assncronos, um servidor diferente subir para cada chamada e, desta forma, um teste no ir interferir no outro.

Podemos agora partir para a criao dos testes:

  1. CEP vlido;
  2. CEP no existe;
  3. CEP invlido;
  4. Erro genrico.

Quando o CEP vlido

Neste teste vamos utilizar a sigil ~s para gerar uma string com escape. Voc pode obter mais detalhes sobre sigils na documentao do Elixir ou no Elixir School.

Veja o resultado da sigil ~s:
Image description

Sobre a funo Bypass.expect e outras similares voc pode encontrar na documentao do Bypass.

...    test "when the cep is valid", %{bypass: bypass} do      cep = "01001000"      url = endpoint_url(bypass.port)      body = ~s({        "cep": "01001-000",        "logradouro": "Praa da S",        "complemento": "lado mpar",        "bairro": "S",        "localidade": "So 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" => "So Paulo",           "logradouro" => "Praa da S",           "siafi" => "7107",           "uf" => "SP"         }}      assert response == expected_response    end...

Se retirarmos a funo Conn.put_resp_header("content-type", "application/json") o middleware do Tesla no ir funcionar e no ir transformar o json:
Image description

Quando o CEP no existe

...    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 invlido

...   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 genrico

Esse teste no conseguiramos simular se estivssemos usando o Tesla.Mock. Vamos simular um timeout, uma falha de comunicao com o servidor.

...    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

Como a reason :econnrefused, poderamos arrumar isso no arquivo client do HTTPoison e o comportamento ficaria igual em ambos os clients:

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

Image description

Criando os testes na regra de negcio com Mox

Como comentamos anteriormente, iremos usar a lib Mox para os testes da criao do usurio. Faz mais sentido usarmos essa abordagem nos testes de negcio, pois essa camada est mais acima do client.

Behaviour para a funo get_cep_info()

O Mox trabalha com behaviours para fazer injeo de dependencia nos testes.

Vamos definir um contrato para a funo get_cep_info(). Quando essa funo for implementada ela obrigatoriamente deve receber uma String como parmetro e retornar sempre uma tupla de {:ok, map()} ou {:error, map()}.

Em lib/my_app/via_cep/behaviour.ex:

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

Se tentarmos retornar algo diferente para esta funo, receberemos um aviso:

Image description

Criando um Mock para implementar o behaviour

Vamos configurar o Mox para criar no teste um mock que tambm implementa esse behaviour.

Em test/test_helper.exs:

ExUnit.start()Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, :manual)# adicione essa linhaMox.defmock(MyApp.ViaCep.ClientMock, for: MyApp.ViaCep.Behaviour)

Client para produo e client para teste (Injeo de Dependncia)

Vamos injetar atravs do ambiente que estamos executando (dev, prod ou test) o Client que iremos utilizar.

Vamos criar uma configurao pra quando voc estiver em ambiente de teste ser usado o mock MyApp.ViaCep.ClientMock, e quando estiver em um ambiente de dev ou produo ser usado a implementao MyApp.ViaCep.TeslaClient.

Em config/config.exs:

...config :my_app,  ecto_repos: [MyApp.Repo]# adicioneconfig :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 configurao, o valor subscrito.

Testando com iex -S mix:

iex> Application.fetch_env!(:my_app, MyApp.Users.Create)[:via_cep_adapter]         MyApp.ViaCep.TeslaClient
  • fetch_env roda em tempo de execuo.
  • compile_env em tempo de compilao.

Em config/test.exs:

...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 invs de Clientconfig :my_app,       MyApp.Users.Create,       via_cep_adapter: MyApp.ViaCep.ClientMock...

Testando com MIX_ENV=test iex -S mix:
Image description

Em lib/my_app/users/create.ex:

  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

Criando os testes de criao de usurio

Em test/my_app/users/create_test.exs:

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" => "So Paulo",           "logradouro" => "Praa 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  endend

Se trocarmos o _cep por 00000000 receberemos um erro:

Image description

No segundo teste "when there are invalid params", no precisemos usar o mock pois a funo get_cep_info() no acionada. O retorno acontece na validao da funo User.validate_before_insert(changeset).

Image description

Concluso

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 negcio, podemos criar contratos explcitos definindo o comportamento de funes usando uma lib como a Mox ou Hammox.


Original Link: https://dev.to/maiquitome/elixir-testando-chamadas-de-uma-api-externa-4b23

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To