Primeiro vamos criar o teste que precisamos para quando o serviço está quebrado. Um erro 500 critico. Vamos adicionar o novo teste em nosso arquivo test/integrations/coffee/client_test.exs
test/integrations/coffee/client_test.exs
defmoduleCoffeeShop.Integrations.Coffee.ClientTestdouseExUnit.CasealiasCoffeeShop.Integrations.Coffee.Client describe "all_hot_coffees/0"do# ... test "service is crashed"do assert {:ok, %Response{status: status}} =Client.all_hot_coffees() assert status ==500endendend
Em nosso teste queremos que o status seja 500. Mas ainda estamos batendo no serviço real. Precisamos configurar o bypass e utilizar em nosso teste, para conseguir simular as respostas que queremos.
test/integrations/coffee/client_test.exs
defmoduleCoffeeShop.Integrations.Coffee.ClientTestdouseExUnit.CasealiasCoffeeShop.Integrations.Coffee.ClientaliasCoffeeShop.Integrations.Coffee.Response describe "all_hot_coffees/0"do setup do bypass =Bypass.open(port: 3000) {:ok, bypass: bypass}end# ... test "service is crashed", %{bypass: bypass} do assert {:ok, %Response{status: status}} =Client.all_hot_coffees() assert status ==500endendend
Adicionamos na função setup o Bypass.open/1, que tem como objetivo abrir um serviço falso. Esse serviço é levantado em http//localhost podendo ser adicionado a porta em nosso caso, escolhemos a porta 3000. Isso pode conflitar com outros serviços em seu computador, então, caso de problema, troque a porta.
Com o serviço de pé, precisamos ajeitar nosso mock. Para criar o mock, precisamos primeiro identificar qual a requisição queremos mockar. Para isso o Bypass possui o método expect_once e expect . Um espera apenas uma chamada, o outro uma ou mais.
test/integrations/coffee/client_test.exs
defmoduleCoffeeShop.Integrations.Coffee.ClientTestdouseExUnit.CasealiasCoffeeShop.Integrations.Coffee.ClientaliasCoffeeShop.Integrations.Coffee.Response describe "all_hot_coffees/0"do setup do bypass =Bypass.open() {:ok, bypass: bypass}end test "respond a list of hot coffees", %{bypass: bypass} do assert {:ok, %Response{status: status}} =Client.all_hot_coffees() assert status ==200end test "service is crashed", %{bypass: bypass} do response ="Server exploded"Bypass.expect_once(bypass,"GET","/coffee/hot",fn conn ->Plug.Conn.resp(conn,500, response)end) assert {:ok, %Response{status: status}} =Client.all_hot_coffees() assert status ==500endendend
Chamamos a função Bypass.expect_once/4, passando a instância do serviço, o verbo que utilizamos, o recurso e a função de callback, que nada mais é que a resposta da requisição. Plug montara uma resposta HTTP para nós. É aqui que a informação falsa deve aparecer. Feito isso, vamos rodar nosso teste
mix test
> mix test .1) test all_hot_coffees/0 service is crashed (CoffeeShop.Integrations.Coffee.ClientTest)test/integrations/coffee/client_test.exs:18Assertionwith==failedcode:assertstatus==500left:200right:500stacktrace:test/integrations/coffee/client_test.exs:26: (test)...Finishedin0.8seconds (0.00s async,0.8ssync)1doctest,4tests,1failure
O teste falhou. Ainda temos um status 200. Isso quer dizer que estamos batendo no serviço real. Com toda certeza, ainda estamos indo para a URL base aplicado pelo plug e o novo serviço mora em http://localhost e precisamos bater la para fins de teste.
Com isso, temos que achar uma forma de poder reconfigurar a URL do nosso cliente quando estamos no teste. Existe a possiblidade de configurar pelas variáveis de ambiente, adicionando um para config/config.exs e um para config/test.exs. Porém, sou bem visual e usar isso:
Não só a urlbase mas todo tipo de configuração gosto de passar pelos parâmetros. Isso facilita a configuração e a utilização para os testes.
O problema agora é que utilizamos o plug para configurar e não conseguimos alcançar ele dessa forma. Precisamos mudar a forma como nosso cliente esta rodando.
Remover plug de configuração
Passar url base para função
Vamos começar removendo o plug para evitar as configurações via macro.
Podemos rodar os testes e ver o tamanho do estrago.
mix test
mix test
Compiling 1 file (.ex)
1) test all_hot_coffees/0 respond a list of hot coffees (CoffeeShop.Integrations.Coffee.ClientTest)
test/integrations/coffee/client_test.exs:13
** (FunctionClauseError) no function clause matching in CoffeeShop.Integrations.Coffee.Response.build/1
The following arguments were given to CoffeeShop.Integrations.Coffee.Response.build/1:
# 1
{:error, {:no_scheme}}
Attempted function clauses (showing 1 out of 1):
def build({:ok, %Tesla.Env{status: status, body: body}})
code: assert {:ok, %Response{status: status}} = Client.all_hot_coffees()
stacktrace:
(coffee_shop 0.1.0) lib/integrations/coffee/response.ex:4: CoffeeShop.Integrations.Coffee.Response.build/1
test/integrations/coffee/client_test.exs:14: (test)
2) test all_hot_coffees/0 service is crashed (CoffeeShop.Integrations.Coffee.ClientTest)
test/integrations/coffee/client_test.exs:18
** (FunctionClauseError) no function clause matching in CoffeeShop.Integrations.Coffee.Response.build/1
The following arguments were given to CoffeeShop.Integrations.Coffee.Response.build/1:
# 1
{:error, {:no_scheme}}
Attempted function clauses (showing 1 out of 1):
def build({:ok, %Tesla.Env{status: status, body: body}})
M: CPU:
code: assert {:ok, %Response{status: status}} = Client.all_hot_coffees()
stacktrace:
(coffee_shop 0.1.0) lib/integrations/coffee/response.ex:4: CoffeeShop.Integrations.Coffee.Response.build/1
test/integrations/coffee/client_test.exs:25: (test)
...
Finished in 0.1 seconds (0.00s async, 0.1s sync)
1 doctest, 4 tests, 2 failures
A vantagem de ter testes, é que saberemos se algo da errado em nossa mudança de forma prática e rápida. Caso quebre, basta resolver, ficando verde, temos um indicativo que tudo voltou a funcionar. Vamos fazer essas belezinhas passarem.
O problema aqui é não ter configurado a url base do cliente, uma vez que removemos o plug. Precisamos ir agora para a segunda parte, montar nossa função de criação do cliente.
Remover plug de configuração
Passar url base para função
Precisamos passar um parâmetro de configuração para dentro de nossa função de requisição all_hot_coffees/1. Utilizaremos uma lista para facilitar a adição de novos argumentos. Utilizaremos Keyword.get/3 para obter o dado da lista:
Keyword.get/3
Primeiro argumento: Lista
Segundo argumento: chave da lista
Terceiro argumento: Valor padrão caso não encontre a chave
Funcionará assim:
defmoduleCoffeeShop.Integrations.Coffee.ClientdouseTeslaaliasCoffeeShop.Integrations.Coffee.Responsedefall_hot_coffees(opts \\ []) do base_url =Keyword.get(opts, :base_url,"https://api.sampleapis.com")"#{base_url}/coffee/hot"|>get()|>Response.build()endend
Passamos o parâmetros de opts para nossa função e dissemos que caso não encontre em opts a chave base_url, utilizar o valor https://api.sampleapis.com. Nossa implementação continuará funcionando com a url base padrão, mas temos a possibilidade de alterar caso necessario, em nosso atual cenário, quando precisar bater no serviço de teste.
Mas isso não é tudo que precisamos fazer. Se rodar o teste estaremos ainda com o problema de sempre acessar o serviço real. Para resolver isso, precisamos passar base_url nos parâmetros da chamada em nosso teste como esperado.
test/integrations/coffee/client_test.exs
defmoduleCoffeeShop.Integrations.Coffee.ClientTestdouseExUnit.CasealiasCoffeeShop.Integrations.Coffee.ClientaliasCoffeeShop.Integrations.Coffee.Response describe "all_hot_coffees/0"do setup do bypass =Bypass.open(port: 3000) {:ok, bypass: bypass}end test "respond a list of hot coffees"do assert {:ok, %Response{status: status}} =Client.all_hot_coffees() assert status ==200end test "service is crashed", %{bypass: bypass} do response ="Server explode"Bypass.expect_once(bypass,"GET","/coffee/hot",fn conn ->Plug.Conn.resp(conn,500, response)end) opts = [ base_url: "http://localhost:3000" ] assert {:ok, %Response{status: status}} =Client.all_hot_coffees(opts) assert status ==500endendend
Rodaresmos o teste
mix test
> mix test
.....
Finished in 0.6 seconds (0.00s async, 0.6s sync)
1 doctest, 4 tests, 0 failures
Randomized with seed 431636
Nada mal em? Criamos um mecanismo simples de configuração de nosso cliente. Podendo adicionar mais opções, que vamos explorar mais a frente. Por hora, conseguimos criar chamadas falsas do jeito que quisermos.
Tem duas coisas me incomodando:
Estamos usando uma macro do Tesla que não tras muito ganho para nós.
Ainda estamos batendo no serviço real em nosso primeiro teste, é interessante não fazer isso
Vamos começar removendo a macro. A macro do Tesla nos da poder de usar algumas funções sem precisar utilizar o prefixo, como se pertencesse a esse modulo. Mas além de criar um acoplamento forte também não fica muito legal visualmente.
"#{base_url}/coffee/hot"|>get()|>Response.build()
Podemos chamar a mesma função usando Tesla.get/1, não precisando da macro.
lib/integrations/coffee/client.ex
defmoduleCoffeeShop.Integrations.Coffee.ClientdoaliasCoffeeShop.Integrations.Coffee.Responsedefall_hot_coffees(opts \\ []) do base_url =Keyword.get(opts, :base_url,"https://api.sampleapis.com")"#{base_url}/coffee/hot"|>Tesla.get()|>Response.build()endend
Sem mais macros. Sem mais magia obscura.
Estamos usando uma macro do Tesla que não trás muito ganho para nós.
Ainda estamos batendo no serviço real em nosso primeiro teste, é interessante não fazer isso
Vamos atualizar nosso teste para que use o dado mockado e pare de realizar a requisição para o serviço real.
test/integrations/coffee/client_test.exs
defmoduleCoffeeShop.Integrations.Coffee.ClientTestdouseExUnit.CasealiasCoffeeShop.Integrations.Coffee.ClientaliasCoffeeShop.Integrations.Coffee.Response describe "all_hot_coffees/0"do setup do bypass =Bypass.open(port: 3000) {:ok, bypass: bypass}end test "respond a list of hot coffees", %{bypass: bypass} do response =""Bypass.expect_once(bypass,"GET","/coffee/hot",fn conn ->Plug.Conn.resp(conn,200, response)end) opts = [ base_url: "http://localhost:3000" ] assert {:ok, %Response{status: status}} =Client.all_hot_coffees(opts) assert status ==200end# ...endend
Uma vez o mecanismo pronto, basta apenas definir o mock e configurar a url base para nosso serviço falso.
Estamos usando uma macro do Tesla que não trás muito ganho para nós.
Ainda estamos batendo no serviço real em nosso primeiro teste, é interessante não fazer isso
Estamos 100% independentes agora. Não irão mais nos cobrar por requisições de teste. Também conseguimos limpar nosso cliente e remover todas os acoplamentos e dependências que não queremos.