Estruturando resposta

Conseguimos nos conectar ao serviço e obtemos a resposta de nosso pedido.

Requisitamos e essa estrutura foi apresentada.

{:ok,
 %Tesla.Env{
   method: :get,
   url: "https://api.sampleapis.com/coffee/hot",
   query: [],
   headers: [
     {"connection", "keep-alive"},
     {"date", "Thu, 11 Apr 2024 18:41:16 GMT"},
     {"etag", "W/\"21df-Qjes7uaeQItDszmliRPvMUXoGIs\""},
     {"server", "cloudflare"},
     {"content-length", "8671"},
     {"content-type", "application/json; charset=utf-8"},
     {"x-powered-by", "Express"},
     {"access-control-allow-origin", "*"},
     {"x-ratelimit-limit", "5000"},
     {"x-ratelimit-remaining", "4999"},
     {"x-ratelimit-reset", "1712861765"},
     {"x-content-type-options", "nosniff"},
     {"cf-cache-status", "DYNAMIC"},
     {"report-to",
      "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=t8jefu1i1%2FKUiH7aYPEOaufCEApMVHn4aHootP6XuJhDF0L5XC6UmY33iprTNR2IXOnbBsNgN7E1a%2Fsuc5rZ9Pm9Qu%2Fp3gtobw4sxZmvrxTR8NXYOdajBzbwmJtJAw%2FG5eu1Pcg%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}"},
     {"nel",
      "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"},
     {"cf-ray", "872d1c1dce920323-GRU"},
     {"alt-svc", "h3=\":443\"; ma=86400"}
   ],
   body: "[{\"title\":\"Black Coffee\",\"description\":\"Svart kaffe är så enkelt som det kan bli med malda kaffebönor dränkta i hett vatten, serverat varmt. Och om du vill låta fancy kan du kalla svart kaffe med sitt rätta namn: café noir.\",\"ingredients\":[\"Coffee\"],\"image\":\"https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":1},{\"title\":\"Latte\",\"description\":\"Som den mest populära kaffedrycken där ute består latte av en skvätt espresso och ångad mjölk med bara en gnutta skum. Den kan beställas utan smak eller med smak av allt från vanilj till pumpa kryddor.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\"],\"image\":\"https://images.unsplash.com/photo-1561882468-9110e03e0f78?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGxhdHRlfGVufDB8fDB8fHww\",\"id\":2},{\"title\":\"Caramel Latte\",\"description\":\"Om du gillar latte med en speciell smak kan karamell latte vara det bästa alternativet för att ge dig en upplevelse av den naturliga sötman och krämigheten hos ångad mjölk och karamell.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Karamellsirap\"],\"image\":\"https://images.unsplash.com/photo-1599398054066-846f28917f38?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":3},{\"title\":\"Cappuccino\",\"description\":\"Cappuccino är en latte som är gjord med mer skum än ångad mjölk, ofta med ett strö av kakaopulver eller kanel på toppen. Ibland kan du hitta variationer som använder grädde istället för mjölk eller sådana som tillsätter smakämnen också.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Foam\"],\"image\":\"https://images.unsplash.com/photo-1557006021-b85faa2bc5e2?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":4},{\"title\":\"Americano\",\"description\":\"Med en liknande smak som svart kaffe består americano av en espresso skott utspätt med hett vatten.\",\"ingredients\":[\"Espresso\",\"Hett vatten\"],\"image\":\"https://images.unsplash.com/photo-1532004491497-ba35c367d634?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":5},{\"title\":\"Espresso\",\"description\":\"Ett espressoskott kan serveras ensamt eller användas som grund för de flesta kaffedrycker, som latte och macchiato.\",\"ingredients\":[\"Espresso\"],\"image\":\"https://images.unsplash.com/photo-1579992357154-faf4bde95b3d?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":6},{\"title\":\"Macchiato\",\"description\":\"Macchiaton är en annan espresso-baserad dryck som har en liten mängd skum på toppen. Det är det glada mellanrummet mellan en cappuccino och en doppio.\",\"ingredients\":[\"Espresso\",\"Foam\"],\"image\":\"https://images.unsplash.com/photo-1557772611-722dabe20327?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":7},{\"title\":\"Mocha\",\"description\":\"För alla chokladälskare där ute kommer ni att bli förälskade i en mocha. Mocha är en choklad-espressodryck med ångad mjölk och skum.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Choklad\"],\"image\":\"https://images.unsplash.com/photo-1607260550778-aa9d29444ce1?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":8},{\"title\":\"Hot Chocolate\",\"description\":\"Under kalla vinterdagar får en kopp varm choklad dig att känna dig bekväm och lycklig. Den får dig också att må bra eftersom den innehåller energigivande koffein.\",\"ingredients\":[\"Choklad\",\"Mjölk\"],\"image\":\"https://images.unsplash.com/photo-1542990253-0d0f5be5f0ed?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDh8fGhvdCUyMGNob2NvbGF0ZXxlbnwwfHwwfHx8MA%3D%3D\",\"id\":9},{\"title\":\"Chai Latte\",\"description\":\"Om du letar efter en smakfull varm dryck mitt i vintern, välj chai latte. Kombinationen av kardemumma och kanel ger en underbar smak.\",\"ingredients\":[\"Te\",\"Mjölk\",\"Ingefära\",\"Kardemumma\",\"Kanel\"],\"image\":\"https://images.u" <> ...,
   status: 200,
   opts: [],
   __module__: CoffeeShop.Integrations.Coffee.Client,
   __client__: %Tesla.Client{fun: nil, pre: [], post: [], adapter: nil}
 }}

Essa é a resposta que o Tesla nos trás.

A primeira coisa que gosto de fazer, é tirar o Tesla de vista. Gosto de criar um estrutura para recebermos a resposta e poder trafegar pelo nosso contexto sem a dependência do Tesla se espalhando.

Para isso criaremos uma estrutura simples com dois atributos

  • status -> responsável por guardaro status da resposta

  • body -> responsável por guardar os dados da requisição

Vamos criar um novo modulo chamada response dentro do contexto coffee

lib/integrations/coffee/response.ex
defmodule CoffeeShop.Integrations.Coffee.Response do
  defstruct status: :integer, body: :map
end

Então, podemos utilizar a estrutura:

%Response{
  status: 200,
  body: "{data: null}"
}

Eu gosto disso para facilitar o entendimento e deixar as coisas mais homogeneas.

Para utilizar essa estrutura, vamos criar uma função chamada build/1 nesse mesmo arquivo Response e lidar com a resposta do Tesla, criando nosso ponto de transição. Para pegar os dados que queremos, vamos utilizar pattern matching e extrair as informações ao mesmo tempo que garantimos que nossa resposta virá com a estrutura esperada. Em seguida, iremos utilizar nossa estrutura de Response para devolver os dados extraídos.

lib/integrations/coffee/response.ex
defmodule CoffeeShop.Integrations.Coffee.Response do
  defstruct status: :integer, body: :map

  def build({:ok, %Tesla.Env{status: status, body: body}}) do
    response = %__MODULE__{
      status: status,
      body: body
    }
    
    {:ok, response}
  end
end

Adicionamos o :ok, como parte da convenção do elixir, você pode ver mais sobre nesse link.

Elegante não? Que acha de um teste para isso?

test/integrations/coffee/response_test.exs
defmodule CoffeeShop.Integrations.Coffee.ResponseTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Response

  describe "build/1" do
    test "build from Tesla.Env structure" do
      tesla_response = {:ok, %Tesla.Env{status: 200, body: "something here"}}

      assert {:ok, %Response{}} = Response.build(tesla_response)
    end
  end
end

Rodaremos o teste para ver ele passando

mix test
> mix test test/integrations/coffee/response_test.exs
.
Finished in 0.02 seconds (0.00s async, 0.02s sync)
1 test, 0 failures

Temos agora um forma estruturada de resposta. Precisamos alterar nosso cliente, esperamos que ele responda nossa nova estrutura. Vamos no teste primeiro. Nosso teste está dizendo que a resposta esperada é um Tesla.Env, mas agora possuímos nosso própria estrutura. Vamos trocar para o Response.

Também podemos fazer a validação do status. Nesse caso receberemos um status 200

test/integrations/coffee/client_test.exs
defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case

  alias CoffeeShop.Integrations.Coffee.Client
  alias CoffeeShop.Integrations.Coffee.Response

  describe "all_hot_coffees/0" do
    test "respond a list of hot coffees" do
      assert {:ok, %Response{status: status}} = Client.all_hot_coffees()
      assert status == 200
    end
  end
end

Algo digno de se notar, acabamos com a dependência do Tesla em nosso teste. Não somos mais dependentes dele, pelo menos aqui no teste e isso é incrível.

Mas se você rodar esse teste, ele vai quebrar.

mix test
> mix test test/integrations/coffee/client_test.exs


  1) test all_hot_coffees/0 respond a list of hot coffees (CoffeeShop.Integrations.Coffee.ClientTest)
     test/integrations/coffee/client_test.exs:13
     match (=) failed
     code:  assert {:ok, %Response{}} = Client.all_hot_coffees()
     left:  {:ok, %CoffeeShop.Integrations.Coffee.Response{}}
     right: {
              :ok,
              %Tesla.Env{
                __client__: %Tesla.Client{fun: nil, pre: [], post: [], adapter: nil},
                __module__: CoffeeShop.Integrations.Coffee.Client,
                body: "[{\"title\":\"Black Coffee\",\"description\":\"Svart kaffe är så enkelt som det kan bli med malda kaffebönor dränkta i hett vatten, serverat varmt. Och om du vill låta fancy kan du kalla svart kaffe med sitt rätta namn: café noir.\",\"ingredients\":[\"Coffee\"],\"image\":\"https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":1},{\"title\":\"Latte\",\"description\":\"Som den mest populära kaffedrycken där ute består latte av en skvätt espresso och ångad mjölk med bara en gnutta skum. Den kan beställas utan smak eller med smak av allt från vanilj till pumpa kryddor.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\"],\"image\":\"https://images.unsplash.com/photo-1561882468-9110e03e0f78?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGxhdHRlfGVufDB8fDB8fHww\",\"id\":2},{\"title\":\"Caramel Latte\",\"description\":\"Om du gillar latte med en speciell smak kan karamell latte vara det bästa alternativet för att ge dig en upplevelse av den naturliga sötman och krämigheten hos ångad mjölk och karamell.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Karamellsirap\"],\"image\":\"https://images.unsplash.com/photo-1599398054066-846f28917f38?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":3},{\"title\":\"Cappuccino\",\"description\":\"Cappuccino är en latte som är gjord med mer skum än ångad mjölk, ofta med ett strö av kakaopulver eller kanel på toppen. Ibland kan du hitta variationer som använder grädde istället för mjölk eller sådana som tillsätter smakämnen också.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Foam\"],\"image\":\"https://images.unsplash.com/photo-1557006021-b85faa2bc5e2?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":4},{\"title\":\"Americano\",\"description\":\"Med en liknande smak som svart kaffe består americano av en espresso skott utspätt med hett vatten.\",\"ingredients\":[\"Espresso\",\"Hett vatten\"],\"image\":\"https://images.unsplash.com/photo-1532004491497-ba35c367d634?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":5},{\"title\":\"Espresso\",\"description\":\"Ett espressoskott kan serveras ensamt eller användas som grund för de flesta kaffedrycker, som latte och macchiato.\",\"ingredients\":[\"Espresso\"],\"image\":\"https://images.unsplash.com/photo-1579992357154-faf4bde95b3d?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":6},{\"title\":\"Macchiato\",\"description\":\"Macchiaton är en annan espresso-baserad dryck som har en liten mängd skum på toppen. Det är det glada mellanrummet mellan en cappuccino och en doppio.\",\"ingredients\":[\"Espresso\",\"Foam\"],\"image\":\"https://images.unsplash.com/photo-1557772611-722dabe20327?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":7},{\"title\":\"Mocha\",\"description\":\"För alla chokladälskare där ute kommer ni att bli förälskade i en mocha. Mocha är en choklad-espressodryck med ångad mjölk och skum.\",\"ingredients\":[\"Espresso\",\"Ångad mjölk\",\"Choklad\"],\"image\":\"https://images.unsplash.com/photo-1607260550778-aa9d29444ce1?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"id\":8},{\"title\":\"Hot Chocolate\",\"description\":\"Under kalla vinterdagar får en kopp varm choklad dig att känna dig bekväm och lycklig. Den får dig också att må bra eftersom den innehåller energigivande koffein.\",\"ingredients\":[\"Choklad\",\"Mjölk\"],\"image\":\"https://images.unsplash.com/photo-1542990253-0d0f5be5f0ed?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDh8fGhvdCUyMGNob2NvbGF0ZXxlbnwwfHwwfHx8MA%3D%3D\",\"id\":9},{\"title\":\"Chai Latte\",\"description\":\"Om du letar efter en smakfull varm dryck mitt i vintern, välj chai latte. Kombinationen av kardemumma och kanel ger en underbar smak.\",\"ingredients\":[\"Te\",\"Mjölk\",\"Ingefära\",\"Kardemumma\",\"Kanel\"],\"image\":\"https://images.u" <> ...,
                headers: [{"connection", "keep-alive"}, {"date", "Thu, 11 Apr 2024 19:37:53 GMT"}, {"etag", "W/\"21df-Qjes7uaeQItDszmliRPvMUXoGIs\""}, {"server", "cloudflare"}, {"content-length", "8671"}, {"content-type", "application/json; charset=utf-8"}, {"x-powered-by", "Express"}, {"access-control-allow-origin", "*"}, {"x-ratelimit-limit", "5000"}, {"x-ratelimit-remaining", "4991"}, {"x-ratelimit-reset", "1712864465"}, {"x-content-type-options", "nosniff"}, {"cf-cache-status", "DYNAMIC"}, {"report-to", "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=7uthBMHN%2FyIQOeGjes6brn7pVz5jkwb6u2f8wsWg5kqNYpwpgL4PmuMSqoptF3GfAcomcro%2BF1Xgb%2BgO7XyYtlwbnqvluFhvMqmfDXyVnzwo0IDw%2Fow23Ab%2B1ID8zcNWqb6Njpo%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}"}, {"nel", "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"}, {"cf-ray", "872d6f0b1f2da415-GRU"}, {"alt-svc", "h3=\":443\"; ma=86400"}],
                method: :get,
                opts: [],
                query: [],
                status: 200,
                url: "https://api.sampleapis.com/coffee/hot"
              }
            }
     stacktrace:
       test/integrations/coffee/client_test.exs:14: (test)


Finished in 0.7 seconds (0.00s async, 0.7s sync)
1 test, 1 failure

O problema é, estamos esperando um %Response{}, mas nosso cliente retornou um %Tesla.Env{}. Precisamos alterar nossa implementação em client.ex. Temos essa implementação:

lib/integrations/coffee/client.ex
defmodule CoffeeShop.Integrations.Coffee.Client do
  use Tesla

  plug(Tesla.Middleware.BaseUrl, "https://api.sampleapis.com")

  def all_hot_coffees do
    get("/coffee/hot")
  end
end

O importante está na linha 7. A função get/1 é do Tesla e sua resposta é um Tesla.Env. Estando na ultima linha da função é o que será retornado. Precisamos utilizar aqui nosso Response.build/1 para retonrnar a estrutura esperada. Vamos fazer essa atualização e aproveitar para deixar as coisas mais bonitas usando pipe.

lib/integrations/coffee/client.ex
defmodule CoffeeShop.Integrations.Coffee.Client do
  use Tesla

  alias CoffeeShop.Integrations.Coffee.Response

  plug(Tesla.Middleware.BaseUrl, "https://api.sampleapis.com")

  def all_hot_coffees do
    "/coffee/hot"
    |> get()
    |> Response.build()
  end
end

Adicionamos nosso Response.build/1 que espera um {:ok, %Tesla.Env{}} e tratamos a resposta para retornar um %Response{} tirando a dependência do Tesla, colocamos a responsabilidade para algo em nosso controle.

Rodaremos novamente o teste

mix test
> mix test
....
Finished in 0.6 seconds (0.00s async, 0.6s sync)
1 doctest, 3 tests, 0 failures

Conclusão

Conseguimos criar uma estrutura de resposta em nosso controle tirando a responsabilidade de uma biblioteca de terceiro. Isso pode parecer um tanto simples e exagerado, mas vai ser de grande ajuda. Tanto para debug quanto para leitura e entendimento.

Agora precisamos falar sobre estrategia de testes. Vamos lá.

Atualizado