Criando o Client

O cliente é parte que cuidará de sua integração. Terá todas as configurações necessárias para conseguirmos nos conectar a um serviço. Também teremos algumas estratégias como politica de Retry e Cache. Ele vai ser nosso contexto para o serviço. Fazendo assim, deixando as coisas mais organizadas.

Vamos do início. Precisamos criar nosso cliente para se comunicar com o SampleApis, um serviço de exemplos de APIs que estamos usando como base em nossos estudos. Ele será responsável em realizar a requisição ao serviço e trazer os dados. Também irá lidar com possíveis erros.

Em nosso primeiro exemplo, queremos trazer a lista do elixir da vida, também chamado de cafézinho quente. Para isso precisamos saber onde estamos pisando.

Vamos fazer uma lista de objetivos. Gosto de listas bem definidas.

O serviço a ser usado encontrasse na URL https://api.sampleapis.com/coffee/hot, e seu verbo HTTP é GET. Isso quer dizer, se você abrir esse link em seu navegador você vai ver uma lista de itens no formato em JSON de cafezinhos quente, como vimos nesse exemplo.

Agora precisamos delegar essa ação para o Tesla. Nossa lista de tarefa então será

  • Obter lista de cafés quentes

  • Testar resposta de sucesso com os cafés quentes

Implementação

Criaremos um novo arquivo dentro de uma pasta integrations. Esse será a pasta do contexto de integrações. Tudo referente a ele viverá ali. Seguirei a ideia de Screaming Archtecture para ter um norte.

mkdir lib/coffee_shop/integrations/coffee

Em sua pasta, criamos um arquivo chamado client.ex referente ao cliente do serviço.

lib/coffee_shop/integrations/coffee/client.ex
defmodule CoffeeShop.Integrations.Coffee.Client do
 
end

Nele vamos criar nossa função de requisição, chamarei ela de all_hot_coffees/0

lib/coffee_shop/integrations/coffee/client.ex
defmodule CoffeeShop.Integrations.Coffee.Client do
  def all_hot_coffees do
  
  end
end

Agora precisamos entender onde devemos requisitar o serviço

GET https://api.sampleapis.com/coffee/hot

Vamos traduzir isso para código.

Precisamos que o tesla execute uma chamada GET para esse recurso. O Tesla possui funçoes de acordo com o verbo que precisamos Tesla.post/2, Tesla.get/1, Tesla.delete/1 e Tesla.put/2 . Obviametne, precisamos do Tesla.get/1 onde seu parâmetro é o recurso que iremos requisitar, sendo https://api.sampleapis.com/coffee/hot.

Tesla.get("https://api.sampleapis.com/coffee/hot")

Uma boa leitura vindo do nosso requisito, certo? Vamos colocar esse código para funcionar.

lib/coffee_shop/integrations/coffee/client.ex
defmodule CoffeeShop.Integrations.Coffee.Client do
  def all_hot_coffees do
    Tesla.get("https://api.sampleapis.com/coffee/hot")
  end
end

Perfeito. Uma coisa que tenho que te falar. Tesla possui o conceito de middlewares. Eles são comportamentos que executam antes de um requisição acontecer. Podemos usar ele para simplificar um pouco o código. Como por exemplo, configurar nossa url base, ao invez de colocar tudo diretamente na função. Para conseguir usar os middlewares de forma limpa, podemos usar uma macro do Tesla e transformar nosso Client em um Tesla.Client. Isso cria uma dependência direta. Não gosto muito disso, mas iremos por esse lado para explorar todo o potencial do Tesla. Com a macro, podemos utilizar plug para configurar os middlewares e utilizaremos o middleware BaseUrl. Com a macro e o middleware, podemos remover um pouco de codigo.

lib/coffee_shop/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

Nossa função ficou mais simples. Vamos continuar assim por um tempo. Com tudo devidamente configurado, podemos rodar nossa função de requisição pelo terminal iterativo. Acesse o terminal.

iex -S mix

Chame a função croiada diretamente.

CoffeeShop.Integrations.Coffee.Client.all_hot_coffees()
iex(1)> CoffeeShop.Integrations.Coffee.Client.all_hot_coffees()
{:ok,
 %Tesla.Env{
   method: :get,
   url: "https://api.sampleapis.com/coffee/hot",
   query: [],
   headers: [
     {"connection", "keep-alive"},
     {"date", "Wed, 10 Apr 2024 20:41:08 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", "4988"},
     {"x-ratelimit-reset", "1712781684"},
     {"x-content-type-options", "nosniff"},
     {"cf-cache-status", "DYNAMIC"},
     {"report-to",
      "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=vdnOEvbwkmUy92e5RTJWIlW8yFKQNctDQEAemw5dNp5KfO8XJWWpDUx7hg%2B%2F99CuPdhTNsilv0kbqA554IsuLjdGRG9kys%2FGX1SpC08NAxF8nrw67hqNs6lhnGsQffS6doqnODg%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}"},
     {"nel",
      "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"},
     {"cf-ray", "87258e520d730319-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}
 }}

Que coisa linda não? Você fez sua primeira requisição a um serviço externo programaticamente. Você consegue ver a lista de cafés e usar em sua aplicação. Para confirmar que tudo está vindo como queremos, você pode ir no atributo body e notará que tem uma resposta em JSON.

Muito mais fácil que você imaginava não? Porém, ainda temos alguns objetivos

  • Obter lista de cafés quentes

  • Testar a resposta de sucesso com os cafés

Teste

Estamos usando um serviço externo para obter dados. Isso nos poupa tempo em relação a desenvolver uma solução. Mas precisamos garantir que nossa implementação sempre venha a funcionar. Para isso, podemos realizar testes programaticamente.

Iremos criar nosso arquivo de teste replicando o caminho da implementação e adicionaremos a base para nossos cenários a serem cobertos.

test/coffee_shop/integrations/coffee/client_test.exs
defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case
  
  describe "all_hot_coffees/0" do
    test "respond a list of hot coffees" do
   
    end
  end
end

Estamos utilizando o ExUnit para nossos testes e com ele, vamos criar nossas afirmações:

test/coffee_shop/integrations/coffee/client_test.exs
defmodule CoffeeShop.Integrations.Coffee.ClientTest do
  use ExUnit.Case
  
  alias CoffeeShop.Integrations.Coffee.Client
  
  describe "all_hot_coffees/0" do
    test "respond a list of hot coffees" do
      assert {:ok, _response} = Client.all_hot_coffees()
    end
  end
end

Rodando o teste

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

Randomized with seed 209276

Precisamos também garantir que a resposta seja um Tesla.Env, estrutura que o Tesla devolve em suas respostas. Vamos adicionar ao nosso teste.

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

  alias CoffeeShop.Integrations.Coffee.Client

  describe "all_hot_coffees/0" do
    test "respond a list of hot coffees" do
      assert {:ok, %Tesla.Env{}} = Client.all_hot_coffees()
    end
  end
end
mix test test/integrations/coffee/client_test.exs
> mix test test/integrations/coffee/client_test.exs
.
Finished in 0.7 seconds (0.00s async, 0.7s sync)
1 test, 0 failures

Conclusão

Nosso primeiro test esta funcionando, isso quer dizer, menos um item na lista. Fácil não? Para que um livro desse? =D

  • Obter dados quando a resposta for um sucesso

  • Tratar resposta quando a resposta for um erro

  • Devemos ter acesso fácil a lista de cafés

Nosso próximo passo é testar o comportamento quando a resposta de nosso serviço volta um erro. A pergunta mais comum aqui é, como diabos eu farei gerar um erro nisso se o serviço não é meu?

Hora do mock.

Atualizado