Quando nos conectamos a um serviço externo, abrimos uma brecha em nossas defesas. Podemos cair em cenários que não esperamos, como algum erro não mapeado ou até mesmo, o serviço parar de responder. Quando isso acontece, precisamos avisar nosso usuário elegantemente que algo deu errado.
Vamos a um exemplo prático. Nosso tratamento esta assim
Agora, vamos pensar. Caso algo de errado, ele vai entrar em nosso build/1, e fazer o JSON.decode!/1. Mas, e se o body, quando der erro, vir em um formato diferente? O que vai acontecer. Vamos atualizar nosso teste:
Precisamos de uma estrutura em JSON, mas nosso serviço não trás esse formato para nós. Temos várias formas de lidar com isso. A mais simples é responder um erro genérico quando receber um status 500. Ou melhor, quando não receber o status esperado. Isso quer dizer, tudo o que não for esperado, vai cair no erro genérico, apenas para não gerar um erro incompreenssivel ou feio para o usuário.
Aqui é um bom local para mapearmos erros não conhecidos e ai sim, implementar uma solução para eles.
Entendido isso, vamos melhorar nosso teste. Nele precisamos saber que
defmoduleCoffeeShop.Integrations.Coffee.ResponseTestdouseExUnit.CasealiasCoffeeShop.Integrations.Coffee.Response describe "build/1"do# ... test "build an 500 error"do tesla_response = {:ok, %Tesla.Env{status: 500, body: "Server exploded"}} assert {:error, %Response{body: _body, error: error}} =Response.build(tesla_response) assert error =="Não foi possível se conectar ao serviço de cafés. Tento novamente mais tarde"endendend
No teste alteramos a assertion da resposta, onde esperamos agora um {:error, %Response{}} para refletir que um erro aconteceu. Tambem adicionamos o error sendo a mensagem que ele retorna.
Vamos em nosso response.ex atualizar a construção de nossa resposta
lib/coffee_shop/integrations/coffee/response.ex
defmoduleCoffeeShop.Integrations.Coffee.Responsedodefstruct status: :integer, body: :map @success_status [200]defbuild({:ok, %Tesla.Env{status: status, body: body}})when status in @success_status do response = %__MODULE__{ status: status, body: JSON.decode!(body) } {:ok, response}endend
Adicionamos o status que podem realizar o build/1. Isso quer dizer, caso um status nao esteja no @success_status, ele não entrará na função.
Feito isso, precisamos criar uma função de mesmo nome para capturar todos os cenários restantes.
lib/coffee_shop/integrations/coffee/response.ex
defmoduleCoffeeShop.Integrations.Coffee.Responsedodefstruct status: :integer, body: :map, error: :string @success_status [200]defbuild({:ok, %Tesla.Env{status: status, body: body}})when status in @success_status do response = %__MODULE__{ status: status, body: JSON.decode!(body) } {:ok, response}enddefbuild({:ok, %Tesla.Env{status: status, body: body}}) do message ="Não foi possível se conectar ao serviço de cafés. Tento novamente mais tarde" response = %__MODULE__{ status: status, error: message } {:error, response}endend
Adicionamos um guard clause na primeira função, liberando apenas para status 200. Todo o restante cairá na segunda função e irá gerar um erro genérico da integração. Tabém adicionamos uma mensagem padrão e retornamos ao invés de :ok, um :error para refletir que algo deu errado.
mix test test/coffee_shop/integrations/coffee/response_test.exs
mix test test/coffee_shop/integrations/coffee/response_test.exs..Finishedin0.6seconds (0.00s async,0.6s sync)1 doctest,4 tests,0 failures
Criamos um mecanismo simples de controle de status, onde podemos gerar N cenários para a resposta que vem do nosso serviço e tratar da melhor forma que quisermos.
Podemos criar outras funções com outras guard clauses para isolar melhor os tipos de resposta.
Ao rodar todos os testes, teremos uma quebra no client_test.exs, isso devido a resposta mudar para uma tupla com :error. Basta altera-la e tudo volta a funciona.