Bypass
Ao testar nossas aplicações, muitas vezes precisamos fazer chamadas a serviços externos. Podemos até mesmo querer simular diferentes situações como erros inesperados do servidor. Tratar isso de modo eficiente não é fácil no Elixir sem uma pequena ajuda.
Nesta lição vamos explorar como bypass pode nos ajudar rapidamente e tratar facilmente essas chamadas em nossos testes
O que é Bypass?
Bypass é descrito como “uma forma rápida de criar um plug customizado que pode substituir um servidor HTTP real para retornar respostas previamente definidas para requisições de clientes.
O que isso significa? Internamente, Bypass é uma aplicação OTP que atua como um servidor externo escutando e respondendo a requisições. Com respostas pré-definidas nós podemos testar qualquer número de possibilidades como interrupções inesperadas de serviço e erros, tudo sem fazer uma única chamada externa.
Usando Bypass
Para melhor ilustrar as funcionalidades do Bypass vamos construir uma aplicação utilitária simples para testar (ping) uma lista de domínios e garantir que eles estão online. Para fazer isso vamos construir um novo projeto supervisor e um GenServer para verificar os domínios em um intervalo configurável. Aproveitando ByPass em nossos testes poderemos verificar se nossa aplicação funcionará em muitos cenários diferentes.
Nota: Se você deseja avançar para o código final, dê uma olhada no repositório Clinic do Elixir School.
Neste ponto devemos estar confortáveis criando novos projetos Mix e adicionando nossas dependências, então focaremos nas partes do código que estamos testando. Se você precisar de uma atualização rápida, consulte a seção New Projects de nossa lição Mix.
Vamos começar criando um novo módulo que tratará de fazer as requisições para nossos domínios.
Com HTTPoison vamos criar uma função, ping/1
, que recebe uma URL e retorna {:ok, body}
para uma requisição HTTP 200 e {:error, reason}
para todos os outros:
defmodule Clinic.HealthCheck do
def ping(urls) when is_list(urls), do: Enum.map(urls, &ping/1)
def ping(url) do
url
|> HTTPoison.get()
|> response()
end
defp response({:ok, %{status_code: 200, body: body}}), do: {:ok, body}
defp response({:ok, %{status_code: status_code}}), do: {:error, "HTTP Status #{status_code}"}
defp response({:error, %{reason: reason}}), do: {:error, reason}
end
Você vai notar que não estamos fazendo um GenServer e isso é por uma boa razão: Separando nossa funcionalidade (e preocupações) do GenServer, podemos testar nosso código sem o obstáculo adicional de concorrência.
Com nosso código pronto, precisamos começar os testes.
Antes de usarmos Bypass precisamos garantir que ele está rodando.
Para fazer isso, vamos atualizar test/test_helper.exs
da seguinte forma:
ExUnit.start()
Application.ensure_all_started(:bypass)
Agora que sabemos que Bypass vai rodar durante nossos testes, vamos avançar para o test/clinic/health_check_test.exs
e terminar nossa configuração.
Para preparar o Bypass para aceitar chamadas precisamos abrir a conexão com Bypass.open/1
, que pode ser feito na nossa callback de configuração do teste:
defmodule Clinic.HealthCheckTests do
use ExUnit.Case
setup do
bypass = Bypass.open()
{:ok, bypass: bypass}
end
end
Por enquanto usaremos Bypass com sua porta padrão, mas se necessitarmos mudá-la (o que faremos mais tarde nessa seção), podemos chamar Bypass.open/1
com a opção :port
e um valor como Bypass.open(port: 1337)
.
Agora estamos prontos para colocar o Bypass para trabalhar.
Vamos começar uma chamada bem sucedida primeiramente:
defmodule Clinic.HealthCheckTests do
use ExUnit.Case
alias Clinic.HealthCheck
setup do
bypass = Bypass.open()
{:ok, bypass: bypass}
end
test "request with HTTP 200 response", %{bypass: bypass} do
Bypass.expect(bypass, fn conn ->
Plug.Conn.resp(conn, 200, "pong")
end)
assert {:ok, "pong"} = HealthCheck.ping("http://localhost:#{bypass.port}")
end
end
Nosso teste é bastante simples e, se o executarmos, veremos que ele passa, mas vamos nos aprofundar e ver o que cada parte está fazendo.
A primeira coisa que vemos em nosso teste é a função Bypass.expect/2
:
Bypass.expect(bypass, fn conn ->
Plug.Conn.resp(conn, 200, "pong")
end)
Bypass.expect/2
recebe nossa conexão Bypass e uma função de aridade simples que se espere que modifique a conexão e a retorne, isso também é uma oportunidade para fazer afirmações na chamada para verificar se ela está conforme esperado. Vamos atualizar a url do nosso teste para incluir /ping
e afirmar (assert) o caminho da chamada e o método HTTP:
test "request with HTTP 200 response", %{bypass: bypass} do
Bypass.expect(bypass, fn conn ->
assert "GET" == conn.method
assert "/ping" == conn.request_path
Plug.Conn.resp(conn, 200, "pong")
end)
assert {:ok, "pong"} = HealthCheck.ping("http://localhost:#{bypass.port}/ping")
end
A última parte do nosso teste que usamos HealthCheck.ping/1
e afirmamos a resposta está conforme esperado, mas do que se trata o bypass.port
?
Bypass está realmente escutando uma porta local e interceptando as requisições, onde estamos usando bypass.port
para retornar a porta padrão uma vez que não definimos uma no Bypass.open/1
.
Em seguida adicionamos casos de teste para erros.
Podemos começar com um teste muito parecido com nosso primeiro, com algumas pequenas mudanças: retornando 500 como código de status e afirmando que a tupla {:error, reason}
é retornada:
test "request with HTTP 500 response", %{bypass: bypass} do
Bypass.expect(bypass, fn conn ->
Plug.Conn.resp(conn, 500, "Server Error")
end)
assert {:error, "HTTP Status 500"} = HealthCheck.ping("http://localhost:#{bypass.port}")
end
Não há nada de especial para este caso de teste, então vamos passar para o próximo: interrupções inesperadas do servidor, estas são as chamadas com as quais estamos mais preocupados.
Para fazer isso usaremos Bypass.down/1
ao invés Bypass.expect/2
para desligar a conexão:
test "request with unexpected outage", %{bypass: bypass} do
Bypass.down(bypass)
assert {:error, :econnrefused} = HealthCheck.ping("http://localhost:#{bypass.port}")
end
Se executarmos nossos testes veremos tudo passando conforme esperado!
Com nosso módulo HealthCheck
testado, podemos seguir em frente e testá-lo juntamente como nosso scheduler baseado no GenServer.
Vários hosts externos
Para nosso projeto manteremos as estruturas do scheduler e usaremos Process.send_after/3
para alimentar nossas verificações reincidentes, para saber mais sobre o módulo Process
dê uma olhada na documentação.
Nosso scheduler necessita de três opções: a coleção de sites, o intervalo de nossas verificações, e o módulo que implementa ping/1
.
Ao passar no nosso módulo, separamos nossa funcionalidade e o nosso GenServer, permitindo-nos testar cada um em isolamento:
def init(opts) do
sites = Keyword.fetch!(opts, :sites)
interval = Keyword.fetch!(opts, :interval)
health_check = Keyword.get(opts, :health_check, HealthCheck)
Process.send_after(self(), :check, interval)
{:ok, {health_check, sites}}
end
Agora precisamos definir a função handle_info/2
para a mensagem :check
enviada send_after/2
.
Para manter as coisas simples vamos passar nossos sites para a HealthCheck.ping/1
e logar nossos resultados com Logger.info
ou Logger.error
no caso de erros.
Vamos configurar nosso código de forma que nos habilite a melhorar a capacidade de relatórios mais tarde:
def handle_info(:check, {health_check, sites}) do
sites
|> health_check.ping()
|> Enum.each(&report/1)
{:noreply, {health_check, sites}}
end
defp report({:ok, body}), do: Logger.info(body)
defp report({:error, reason}) do
reason
|> to_string()
|> Logger.error()
end
Conforme discutido, passamos nossos sites para a HealthCheck.ping/1
então iteramos os resultados com Enum.each/2
aplicando nossa função report/1
em cada uma delas.
Com essas funções nosso scheduler está pronto e podemos nos concentrar em testá-lo.
Não vamos focar muito em fazer testes unitários para os schedulers uma vez que eles não necessitam Bypass, então podemos pular para o código final:
defmodule Clinic.SchedulerTest do
use ExUnit.Case
import ExUnit.CaptureLog
alias Clinic.Scheduler
defmodule TestCheck do
def ping(_sites), do: [{:ok, "pong"}, {:error, "HTTP Status 404"}]
end
test "health checks are run and results logged" do
opts = [health_check: TestCheck, interval: 1, sites: ["http://example.com", "http://example.org"]]
output =
capture_log(fn ->
{:ok, _pid} = GenServer.start_link(Scheduler, opts)
:timer.sleep(10)
end)
assert output =~ "pong"
assert output =~ "HTTP Status 404"
end
end
Confiamos em uma implementação de teste de nossos health checks
com TestCheck
juntamente com CaptureLog.capture_log/1
para afirmar que as mensagens apropriadas são logadas.
Agora trabalhamos nos módulos Scheduler
e HealthCheck
, vamos escrever um teste de integração para verificar tudo funcionando junto. Precisaremos do Bypass para este teste e teremos que tratar múltiplas chamadas com Bypass por teste, veremos como fazer isso.
Lembra do bypass.port
de mais cedo? Quando precisamos simular múltiplos sites, a opção :port
vem a calhar. Como você provavelmente adivinhou, podemos criar múltiplas conexões Bypass cada uma com uma porta diferente, estas que simularão sites independentes. Começaremos revisando nosso arquivo atualizado test/clinic_test.exs
:
defmodule ClinicTest do
use ExUnit.Case
import ExUnit.CaptureLog
alias Clinic.Scheduler
test "sites are checked and results logged" do
bypass_one = Bypass.open(port: 1234)
bypass_two = Bypass.open(port: 1337)
Bypass.expect(bypass_one, fn conn ->
Plug.Conn.resp(conn, 500, "Server Error")
end)
Bypass.expect(bypass_two, fn conn ->
Plug.Conn.resp(conn, 200, "pong")
end)
opts = [interval: 1, sites: ["http://localhost:1234", "http://localhost:1337"]]
output =
capture_log(fn ->
{:ok, _pid} = GenServer.start_link(Scheduler, opts)
:timer.sleep(10)
end)
assert output =~ "[info] pong"
assert output =~ "[error] HTTP Status 500"
end
end
Não deve haver nada de tão surpreendente no teste acima. Ao invés de criar uma simples conexão Bypass no setup
, estamos criando duas no teste e especificando suas portas como 1234 e 1337. Em seguida, vemos nossas chamadas Bypass.expect/2
e finalmente o mesmo código que temos no SchedulerTest
para iniciar o scheduler e afirmar que logamos as mensagens apropriadas.
É isso aí! Construímos um utilitário para nos manter informados se houver qualquer problema em nossos domínios e aprendemos como usar o Bypass para escrever melhores testes com serviços externos.
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!