An Interest In:
Web News this Week
- April 2, 2024
- April 1, 2024
- March 31, 2024
- March 30, 2024
- March 29, 2024
- March 28, 2024
- March 27, 2024
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:
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:
- CEP vlido;
- CEP no existe;
- CEP invlido;
- 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.
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:
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
:
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
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:
Se tentarmos retornar algo diferente para esta funo, receberemos um aviso:
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
:
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)
:
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:
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)
.
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
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To