https://a.storyblok.com/f/270183/89980/f1522994f2/webmock_vcr.png

Testando APIs externas em Ruby com Webmock e VCR

Publicado em November 18, 2021

Tempo de leitura: 13 minutos

Os testes automatizados há muito tempo são parte integrante do processo de desenvolvimento e implantação de software, e seus benefícios são amplamente reconhecidos. Entre outras coisas, utilizamos os testes para garantir a qualidade do código, para evitar regressões e, no contexto do Desenvolvimento Orientado a Testes (TDD), como parte do processo de escrita de código.

Como desenvolvedores, estamos familiarizados e nos sentimos à vontade com o conceito de escrever conjuntos de testes automatizados como parte do nosso fluxo de trabalho de desenvolvimento. No entanto, quando nosso aplicativo utiliza APIs externas, escrever esses testes traz um conjunto específico de desafios. Vamos explorar alguns deles com um exemplo.

Exemplo: Envio de um SMS com a Messages API do Vonage

Imagine um cenário em que estejamos desenvolvendo um aplicativo que inclua a funcionalidade de enviar mensagens de texto via SMS. Esse seria um excelente caso de uso para a Messages API da Vonage. Para implementar essa funcionalidade usando a Messages API, nosso aplicativo poderia, por exemplo, incluir uma MessagesClient classe que defina um send_sms método. O objetivo desse método seria enviar uma POST para utilizar o ponto de extremidade da Messages API:

https://api.nexmo.com/v1/messages

No que diz respeito às dependências necessárias para os testes, nosso Gemfile poderia ser algo assim (embora, na realidade, nosso aplicativo provavelmente incluísse algumas dependências adicionais).

Gemfile

# Gemfile

source "https://rubygems.org"
ruby "3.0.0"

gem 'faraday'

group :test do
  gem 'rspec'
end

O Faraday é uma biblioteca Ruby para gerenciar solicitações e respostas HTTP.

Nosso arquivo de aplicativo definiria a MessagesClient classe com seu send_sms método.

app.rb

# app.rb
require "json"

class MessagesClient
  URI = 'https://api.nexmo.com/v1/messages'

  def send_sms(from_number, to_number, message)
    headers = generate_headers(message)
    body = {
      message_type: 'text',
      channel: 'sms',
      from: from_number,
      to: to_number,
      text: message
    }
    Faraday.new.post(URI, body.to_json, headers)
  end

  # additional methods omitted for brevity

end

No exemplo acima, nosso send_sms método define parâmetros para o texto da mensagem, o número de destino e o número de origem. Ele inclui esses detalhes em uma mensagem body que é enviada em uma POST solicitação ao endpoint da API usando o método post . O send_sms método, então, retorna um objeto que representa a resposta HTTP recebida pela instância do Faraday.

Então, como faríamos para testar esse método? Uma abordagem para testar o “happy path” seria escrever um teste que chamasse o método, acessando assim o endpoint da API em produção, e verificasse se recebemos um código de resposta que indicasse sucesso. No caso da Messages API v1, esse seria um código de resposta HTTP de 202.

messages_client_spec.rb

require "spec_helper"

describe MessagesClient do
  let(:app) { MessagesClient.new }

  describe "#send_sms" do
    let(:from_number) { "447700900000" }
    let(:to_number) { "447700900001" }
    let(:message) { "Hello world!" }

    it "returns status 202 Accepted" do
      response = app.send_sms(from_number, to_number, message)

      expect(response.status).to eq 202
    end
  end
end

Existem alguns problemas com essa abordagem de teste.

Em primeiro lugar, qualquer teste que interaja com uma dependência externa, como um banco de dados ou uma API externa, provavelmente será muito mais lento do que um que não o faça. Se analisarmos um exemplo de execução desse teste, podemos ver que ele leva 1.63 segundos.

Finished in 1.63 seconds (files took 0.36557 seconds to load)
1 example, 0 failures

Embora isso possa não parecer tão lento assim, trata-se de um único teste para um único método. Dependendo do tamanho e da complexidade do nosso aplicativo, poderíamos ter inúmeros testes semelhantes. Nesse cenário, a execução de todo o conjunto de testes se tornaria extremamente lenta.

Em segundo lugar, como nosso teste depende do envio de uma solicitação HTTP pela rede e do processamento dessa solicitação por uma dependência externa, não podemos ter certeza de que receberemos uma resposta válida, nem mesmo de que receberemos qualquer resposta. Podem ocorrer problemas de rede, interrupções temporárias ou outros fatores externos fora do nosso controle, o que significa que talvez não recebamos a resposta esperada sempre que que executarmos nosso teste.

Há muita discussão sobre o tema das melhores práticas para a criação de testes automatizados e sobre o que os diferentes tipos de testes devem ou não fazer. No entanto, alguns princípios geralmente aceitos, especialmente ao se trabalhar com testes de unidade e pequenos testes de integração, indicam que devemos tentar fazer com que nossos testes rápidos e determinísticos.

Quanto mais descemos na “pirâmide de testes”, mais testes temos e com maior frequência os executamos. Testes rápidos são, portanto, importantes nesse nível. Além disso, ao usar testes como parte do processo de desenvolvimento ou para obter feedback antecipado, é importante que nossos testes sejam determinísticos; em outras palavras, queremos ter certeza de que, quando recebem uma entrada específica, o teste deve produzir uma saída pré-determinada.

Ao considerar esses princípios no contexto do nosso send_sms teste, isso apresenta um problema. Queremos que nosso teste seja rápido e determinístico, mas as questões envolvidas no acesso ao endpoint da API ativa vão contra esses princípios.

Existem outras considerações possíveis ao usar uma API externa, como métodos HTTP não idempotentes (por exemplo, a POST solicitação em nosso método criará mensagens SMS reais sempre que executarmos nosso teste), ou possíveis problemas relacionados a custos ou limites de taxa. Muitas dessas questões adicionais podem ser resolvidas com o uso de uma sandbox de API, e a Messages API oferece uma sandbox para alguns canais de mensagens.

O uso de uma sandbox é mais relevante para testes em níveis mais altos da pirâmide, como testes de ponta a ponta ou funcionais e alguns testes de integração de maior porte, e não resolve de fato nossas questões de velocidade e determinismo. O que realmente queremos para nossos testes de nível inferior é uma maneira de evitar completamente o uso de dependências externas. Uma solução para isso é simular.

Zombaria

Para quem não está familiarizado com o termo, o “mocking” é uma técnica de teste na qual uma resposta simulada ou “falsa” é usada como substituto de uma resposta real proveniente de uma parte interna ou externa do nosso aplicativo. Em nível interno, isso pode significar que um objeto simulado seja retornado por uma chamada de método ou função. No contexto de APIs externas, geralmente significa substituir uma resposta HTTP real por uma simulada.

Uma grande vantagem dessa abordagem ao trabalhar com uma API externa é que, ao não enviar uma solicitação pela rede e não precisar aguardar a resposta, a execução de um teste se torna muito mais rápida. Além disso, ao predefinir a resposta HTTP, tornamos nossos testes determinísticos.

O Mocking parece ser a solução ideal para resolver os problemas que identificamos em nossa configuração atual de testes; então, como podemos implementá-lo em nosso send_sms teste?

Apresentando o Webmock

Webmock é uma biblioteca Ruby para simulação e definição de expectativas em solicitações HTTP. Ela oferece suporte a diversas bibliotecas HTTP e pode ser integrada a várias estruturas de teste, incluindo rspec.

Como modelo mental de alto nível, o Webmock basicamente faz o seguinte:

  • Intercepta todas as solicitações HTTP de saída feitas pelo nosso aplicativo

  • Compara essas solicitações com um “stub” pré-registrado

  • Retorna uma resposta predefinida para essa solicitação, em vez da resposta HTTP real

Vamos explorar esse modelo mental na prática, adicionando o Webmock à nossa configuração de teste.

Exemplo atualizado: Simulando nossas respostas HTTP com o Webmock

Primeiro, precisamos adicionar webmock ao nosso Gemfile e executar bundle install.

Gemfile

# Gemfile

source "https://rubygems.org"
ruby "3.0.0"

gem 'faraday'

group :test do
  gem 'rspec'
  gem 'webmock'
end

Observação: também precisamos adicionar require 'webmock/rspec' ao nosso spec_helper.

Podemos, então, atualizar nosso teste para usar o Webmock.

messages_client_spec.rb

require "spec_helper"

describe MessagesClient do
  let(:app) { MessagesClient.new }

  describe "#send_sms" do
    let(:from_number) { "447700900000" }
    let(:to_number) { "447700900001" }
    let(:message) { "Hello world!" }

    it "returns status 202 Accepted" do
      stub_request(:post, "https://api.nexmo.com/v1/messages").to_return(status: 202)
      response = app.send_sms(from_number, to_number, message)

      expect(response.status).to eq 202
    end
  end
end

Nosso teste atualizado registra um stub, utilizando o método stub_request do Webmock. O stub está configurado para corresponder a qualquer POST solicitação para https://api.nexmo.com/v1/messagese retorne um :status de 202.

O Webmock intercepta a solicitação HTTP de saída e, em vez disso, retorna a resposta pré-determinada; portanto, executar nosso teste é muito mais rápido do que antes:

Finished in 0.00749 seconds (files took 0.50786 seconds to load)
1 example, 0 failures

Limitações da simulação

A adoção do Webmock tornou nossos testes rápidos e determinísticos, e, por isso, parece ser uma boa opção para nossas necessidades de teste. O uso de uma ferramenta de simulação para testar APIs externas, no entanto, traz algumas ressalvas.

Escrever mocks pode ser demorado

Nosso mock de exemplo define apenas o :status código para a resposta. Outros mocks podem precisar definir também a resposta :headers e/ou :body. Dependendo da API que está sendo testada, esses cabeçalhos e o corpo podem ser bastante extensos e complexos, exigindo um tempo considerável para serem definidos na simulação.

Se ampliarmos isso para vários testes em vários pontos de extremidade da API, logo poderemos ter um investimento significativo de tempo para escrever essas simulações.

Os mocks podem ser difíceis de manter

Relacionado a esse primeiro ponto está a manutenção. As APIs externas podem sofrer alterações ao longo do tempo, com a adição de novos recursos ou o lançamento de novas versões. Por exemplo, a Messages API do Vonage lançou recentemente uma nova versão. Se tivermos um grande número de simulações complexas para nossos testes, manter essas simulações atualizadas de acordo com as mudanças pode exigir muito tempo e esforço, que poderiam ser melhor investidos em outras áreas.

Os mocks podem partir de suposições incorretas sobre as dependências

Como os mocks são escritos para representar uma resposta específica, em vez de serem um resposta real , elas se baseiam necessariamente em suposições sobre como seria essa resposta real. Isso pode não ser um problema para o nosso teste de exemplo, mas, à medida que as respostas que queremos simular aumentam em complexidade, também aumenta a possibilidade de fazermos uma suposição incorreta sobre essa resposta. Tais suposições incorretas poderiam levar a um código que passa no teste simulado, mas não funciona corretamente. Esperamos ter testes em níveis mais altos da pirâmide que identifiquem tais problemas, mas, idealmente, queremos detectá-los o mais cedo possível com nossos testes de nível mais baixo.

Para contornar essas limitações, podemos recorrer a outra biblioteca do Ruby: a VCR.

Videocassete

VCR é uma biblioteca que segue o padrão de teste “gravação e reprodução” no contexto de solicitações e respostas HTTP. Essencialmente, ela grava as interações HTTP de um conjunto de testes e as reproduz durante execuções futuras dos testes.

O VCR implementa esse padrão utilizando o conceito de “cassetes” (baseado nas fitas cassete do obsoleto tecnologia do gravador de fita de vídeo ). Cada “fita” é um arquivo que contém dados representando a gravação de uma interação HTTP específica. Na primeira vez em que um teste é executado, ocorre um ciclo real de solicitação/resposta HTTP, e os detalhes desse ciclo são registrados como uma fita.

A cassete inclui informações tanto sobre a solicitação quanto sobre a resposta. Os dados da solicitação são usados para a correspondência de solicitações durante execuções de teste subsequentes, e os dados da resposta são usados para simular a resposta esperada nessas execuções. Como as “simulações” utilizam dados de resposta reais dados de resposta reais, isso resolve as questões descritas anteriormente relacionadas ao tempo necessário para escrever e manter simulações e às suposições feitas nesse processo.

Talvez seja mais fácil visualizar como o VCR funciona se analisarmos isso no contexto de nossa configuração de teste.

Exemplo atualizado: Integrando o VCR à nossa configuração de testes

Primeiro, precisamos adicionar vcr ao nosso Gemfile e executar bundle install

Gemfile

# Gemfile

source "https://rubygems.org"
ruby "3.0.0"

gem 'faraday'

group :test do
  gem 'rspec'
  gem 'webmock'
  gem 'vcr'
end

Podemos, então, atualizar nosso teste para usar o VCR.

messages_client_spec.rb

require "spec_helper"
require "vcr"

VCR.configure do |config|
  config.cassette_library_dir = "spec/cassettes"
  config.hook_into :webmock
  config.configure_rspec_metadata!
end

describe MessagesClient do
  let(:app) { MessagesClient.new }

  describe "#send_sms" do
    let(:from_number) { "447700900000" }
    let(:to_number) { "447700900001" }
    let(:message) { "Hello world!" }

    it "returns status 202 Accepted" do
      response = VCR.use_cassette('send_sms') do
        app.send_sms(from_number, to_number, message)
      end

      expect(response.status).to eq 202
    end
  end
end

Neste arquivo, precisamos vcr e, em seguida, configuramos nossas definições do VCR (embora, se tivéssemos vários arquivos de especificações, pudéssemos mover a configuração para o nosso spec_helper arquivo).

  • A cassette_library_dir configuração indica ao VCR onde armazenar as “fitas”. Aqui, especificamos um diretório spec/cassettes; caso esse diretório não exista, o VCR o criará.

  • A hook_into configuração informa ao VCR como se integrar às solicitações HTTP. Como já temos webmock como dependência, nós o especificamos aqui (embora pudéssemos removê-lo de nosso Gemfile e nos conectar diretamente ao Faraday).

Há também muitas outras opções de configuração disponíveis.

No próprio teste, removemos a solicitação simulada pelo Webmock. Em vez disso, definimos a response variável para o valor de retorno do método do VCR use_cassette , para o qual passamos um nome de cassete como argumento e também um bloco. Na configuração padrão de gravação, se a fita existir, o VCR a utilizará para construir um objeto de resposta que poderemos então verificar em nosso teste. Se a fita não existir, o VCR chamará o bloco e usará o que ele retornar para criar a fita.

Quando nós executamos executamos nosso teste, como a cassete ainda não existe nesse momento, acessamos o endpoint da API e o VCR usa essa interação HTTP para criar um send_sms.yml arquivo, que armazena os detalhes da solicitação e da resposta HTTP.

send_sms.yml

---
http_interactions:
- request:
    method: post
    uri: https://api.nexmo.com/v1/messages
    body:
      encoding: UTF-8
      string: '{"message_type":"text","channel":"sms","from":"447700900000","to":"447700900001","text":"Hello
        world!"}'
    headers:
      User-Agent:
      - Faraday v1.8.0
      Authorization:
      - Basic xxxxxxxxxxxxxxxxxxxxxxxxxxx==
      Content-Type:
      - application/json
      Host:
      - api.nexmo.com
      Content-Length:
      - '12'
      Accept-Encoding:
      - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
      Accept:
      - "*/*"
  response:
    status:
      code: 202
      message: Accepted
    headers:
      Server:
      - nginx
      Date:
      - Thu, 18 Nov 2021 12:35:49 GMT
      Content-Type:
      - application/json
      Content-Length:
      - '55'
    body:
      encoding: UTF-8
      string: '{"message_uuid":"c26213be-2916-4c64-903e-4125158eedd8"}'
  recorded_at: Thu, 18 Nov 2021 12:35:49 GMT
recorded_with: VCR 6.0.0

Quando o teste é executado pela primeira vez, como estamos acessando o endpoint da API, a execução é tão lenta quanto quando não estávamos usando nem o Webmock nem o VCR.

Finished in 1.61 seconds (files took 0.58899 seconds to load)
1 example, 0 failures

Em todas as execuções de teste execuções de teste, o VCR utilizará a fita. Primeiro, ele verificará se os detalhes da solicitação para a execução do teste correspondem aos registrados na fita. Se corresponderem , os detalhes da resposta da fita serão usados para criar um objeto de resposta. Nesse caso, o código de resposta registrado é 202 e, portanto, este será definido como o status para o nosso objeto de resposta. Como nosso teste está verificando se response.status deve ser igual a 202, nosso teste é aprovado.

Esses testes subsequentes também são muito mais rápidas do que a primeira.

Finished in 0.01033 seconds (files took 0.55884 seconds to load)
1 example, 0 failures

Dicas e truques para o videocassete

O VCR oferece muita flexibilidade em termos de opções de configuração.

Configurar a correspondência de solicitações

Para reproduzir uma gravação, o VCR precisa comparar novas solicitações HTTP com os detalhes de uma gravação anterior. Isso comparação pode ser feita com base em vários elementos de uma solicitação.

A configuração padrão consiste em realizar a correspondência com base no método HTTP e na URI, mas essa configuração pode ser alterada para realizar a correspondência separadamente com o host e o caminho (em vez da URI completa), os parâmetros de consulta, os cabeçalhos da solicitação e o corpo da solicitação.

Além disso, é possível criar comparadores personalizados para oferecer ainda mais flexibilidade.

Grave de novo, não deixe desaparecer

Como mencionado anteriormente, uma API externa pode sofrer alterações ao longo do tempo ou lançar novas versões, o que significa que as gravações podem ficar “desatualizadas”. Se isso acontecer, em vez de termos que reescrever um mock inteiro (como faríamos em uma configuração padrão de mocking), podemos gravar uma nova interação HTTP para substituir a desatualizada. Existem algumas maneiras de abordar a regravação:

  • A maneira mais direta é excluir o arquivo da gravação atual. Se não houver nenhuma gravação para um teste específico, o VCR gravará automaticamente uma nova.

  • Existem também vários modos de gravação que podem ser configurados para determinar quando novas gravações são feitas. Por exemplo, :once (que é o padrão) grava novas interações apenas se não houver um arquivo de cassete, enquanto :new_episodes gravará uma nova interação se houver um arquivo existente para um teste, mas os detalhes da solicitação desse teste não corresponderem exatamente aos registrados no arquivo.

  • Podemos ativar a regravação automática para regravar as interações em intervalos regulares. Podemos definir a :re_record_interval opção na configuração de uma fita específica. Quando essa fita for utilizada, o videocassete verificará o recorded_at marca de data e hora na fita em relação à hora atual. Se tiver decorrido mais tempo do que o especificado pela :re_record_interval, a interação será regravada.

Cuidado com os dados confidenciais

Ao interagir com uma API externa, dependendo do método de autenticação dessa API, podemos muito bem estar incluindo dados confidenciais, como chaves de API, como parte de nossas solicitações, como, por exemplo, em um cabeçalho de autorização . Esses dados estarão presentes na gravação do VCR como parte da interação. Se também estivermos disponibilizando o código do nosso projeto publicamente, por exemplo, enviando-o para um repositório público no GitHub, isso pode nos causar um problema.

Uma solução poderia ser adicionar gravações individuais ou todo o nosso cassettes diretório inteiro a um .gitignore arquivo. Como alternativa, podemos utilizar as filter_sensitive_data opção de configuração para especificar uma sequência de substituição para determinados dados, que será exibida na gravação no lugar dos dados reais.

Consulte a documentação

O VCR oferece algumas documentação detalhada de uso sobre essas e muitas outras opções de configuração, bem como documentação da API para a própria biblioteca.

Ferramentas alternativas

As ferramentas abordadas aqui são bem conhecidas no ecossistema do Ruby, mas também há muitas alternativas disponíveis, tanto para quem trabalha com Ruby quanto para quem não trabalha.

No que diz respeito aos recursos de simulação, a rspec-mocks biblioteca pode oferecer recursos de simulação para rspec. Algumas bibliotecas HTTP, como a Faraday , oferecem adaptadores que permitem definir solicitações simuladas. A funcionalidade de simulação do Faraday (entre outras) também é compatível com o VCR.

Além disso, existem muitas versões do VCR portadas para outras linguagens de programação, tais como vcrpy para Python, php-vcr para PHP e scotch e Betamax.Net para .NET/C#. O Nock oferece funcionalidades semelhantes à combinação do VCR com o Webmock para o Node.

Há muitas outras versões para outras línguas listadas no VCR README; portanto, seja qual for o idioma que você use, deve haver, com sorte, uma ferramenta de gravação e reprodução à sua disposição.

Boa sorte nos testes!

Compartilhar:

https://a.storyblok.com/f/270183/373x376/e8d3211236/karl-lingiah.png
Karl LingiahDefensor da Comunidade de Desenvolvedores Ruby

Karl é um Developer Advocate da Vonage, com foco na manutenção de nossos SDKs de servidor em Ruby e na melhoria da experiência dos desenvolvedores da nossa comunidade. Ele adora aprender, criar coisas, compartilhar conhecimento e tudo o que esteja relacionado à tecnologia da web em geral.