Teste de solicitação HTTP: uma ferramenta de sobrevivência do desenvolvedor
Publicados: 2022-03-11O que fazer quando um conjunto de testes não é viável
Há momentos em que nós – programadores e/ou nossos clientes – temos recursos limitados para escrever tanto a entrega esperada quanto os testes automatizados para essa entrega. Quando o aplicativo é pequeno o suficiente, você pode cortar custos e pular testes porque você lembra (principalmente) o que acontece em outras partes do código quando você adiciona um recurso, corrige um bug ou refatora. Dito isso, nem sempre trabalharemos com aplicativos pequenos, além disso, eles tendem a ficar maiores e mais complexos com o tempo. Isso torna o teste manual difícil e super irritante.
Nos meus últimos projetos, fui forçado a trabalhar sem testes automatizados e, honestamente, foi embaraçoso ter o cliente me enviando um e-mail após um push de código para dizer que o aplicativo estava quebrando em lugares onde eu nem havia tocado no código.
Portanto, nos casos em que meu cliente não tinha orçamento ou intenção de adicionar qualquer estrutura de teste automatizada, comecei a testar a funcionalidade básica de todo o site enviando uma solicitação HTTP para cada página individual, analisando os cabeçalhos de resposta e procurando o '200' resposta. Parece claro e simples, mas há muito que você pode fazer para garantir a fidelidade sem precisar escrever nenhum teste, unidade, funcional ou integração.
Testes Automatizados
No desenvolvimento web, os testes automatizados compreendem três tipos principais de testes: testes unitários, testes funcionais e testes de integração. Frequentemente, combinamos testes de unidade com testes funcionais e de integração para garantir que tudo corra bem como um aplicativo completo. Quando esses testes são executados em uníssono, ou sequencialmente (de preferência com um único comando ou clique), passamos a chamá-los de testes automatizados, unitários ou não.
Em grande parte, o objetivo desses testes (pelo menos no desenvolvimento da Web) é garantir que todas as páginas do aplicativo sejam renderizadas sem problemas, livres de erros ou bugs fatais (interrupção do aplicativo).
Teste de unidade
O teste de unidade é um processo de desenvolvimento de software no qual as menores partes do código – unidades – são testadas independentemente para operação correta. Aqui está um exemplo em Ruby:
test “should return active users” do active_user = create(:user, active: true) non_active_user = create(:user, active: false) result = User.active assert_equal result, [active_user] end
Teste funcional
O teste funcional é uma técnica usada para verificar os recursos e a funcionalidade do sistema ou software, projetada para cobrir todos os cenários de interação do usuário, incluindo caminhos de falha e casos limite.
Nota: todos os nossos exemplos estão em Ruby.
test "should get index" do get :index assert_response :success assert_not_nil assigns(:object) end
Teste de integração
Uma vez que os módulos são testados unitariamente, eles são integrados um a um, sequencialmente, para verificar o comportamento combinacional e validar se os requisitos estão implementados corretamente.
test "login and browse site" do # login via https https! get "/login" assert_response :success post_via_redirect "/login", username: users(:david).username, password: users(:david).password assert_equal '/welcome', path assert_equal 'Welcome david!', flash[:notice] https!(false) get "/articles/all" assert_response :success assert assigns(:articles) end
Testes em um mundo ideal
O teste é amplamente aceito na indústria e faz sentido; bons testes permitem que você:
- A qualidade garante toda a sua aplicação com o mínimo de esforço humano
- Identifique bugs com mais facilidade porque você sabe exatamente onde seu código está falhando devido a falhas de teste
- Crie documentação automática para o seu código
- Evite 'codificar constipação', que, de acordo com um cara do Stack Overflow, é uma maneira bem-humorada de dizer: “quando você não sabe o que escrever em seguida, ou tem uma tarefa difícil pela frente, comece escrevendo pequenas .”
Eu poderia continuar falando sobre como os testes são incríveis, e como eles mudaram o mundo e yada yada yada, mas você entendeu. Conceitualmente, os testes são incríveis.
Testes no mundo real
Embora haja méritos em todos os três tipos de teste, eles não são escritos na maioria dos projetos. Por quê? Bem, deixe-me resumir:
Tempo/Prazos
Todo mundo tem prazos, e escrever novos testes pode atrapalhar o cumprimento de um. Pode levar tempo e meio (ou mais) para escrever uma aplicação e seus respectivos testes. Agora, alguns de vocês não concordam com isso, citando tempo economizado em última análise, mas eu não acho que esse seja o caso e explicarei o porquê em 'Diferença de Opinião'.
Problemas do cliente
Muitas vezes, o cliente não entende realmente o que é teste ou por que ele tem valor para o aplicativo. Os clientes tendem a se preocupar mais com a entrega rápida do produto e, portanto, veem os testes programáticos como contraproducentes.
Ou pode ser tão simples como o cliente não ter orçamento para pagar o tempo extra necessário para implementar esses testes.
Falta de conhecimento
Existe uma tribo considerável de desenvolvedores no mundo real que não sabe que existe teste. Em cada conferência, encontro, concerto, (mesmo nos meus sonhos), encontro desenvolvedores que não sabem escrever testes, não sabem o que testar, não sabem como configurar a estrutura para testes e assim em. Os testes não são exatamente ensinados nas escolas e pode ser um incômodo configurar/aprender a estrutura para executá-los. Então, sim, há uma barreira definitiva à entrada.
'É um monte de trabalho'
Escrever testes pode ser avassalador para programadores novos e experientes, mesmo para aqueles gênios que mudam o mundo, e ainda por cima, escrever testes não é empolgante. Pode-se pensar: “Por que devo me envolver em um trabalho desinteressante quando posso estar implementando um recurso importante com resultados que impressionarão meu cliente?” É um argumento difícil.
Por último, mas não menos importante, é difícil escrever testes e os alunos de ciência da computação não são treinados para isso.
Ah, e refatorar com testes de unidade não é divertido.
Diferença de opinião
Na minha opinião, o teste de unidade faz sentido para lógica algorítmica, mas não tanto para coordenar código vivo.
As pessoas afirmam que, embora você esteja investindo tempo extra antecipadamente escrevendo testes, você economiza horas depois ao depurar ou alterar o código. Eu discordo e ofereço uma pergunta: seu código é estático ou está sempre mudando?
Para a maioria de nós, está sempre mudando. Se você está escrevendo um software de sucesso, está sempre adicionando recursos, alterando os existentes, removendo-os, consumindo-os, o que quer que seja, e para acomodar essas alterações, você deve continuar alterando seus testes, e alterar seus testes leva tempo.
Mas, você precisa de algum tipo de teste
Ninguém argumentará que a falta de qualquer tipo de teste é o pior caso possível. Depois de fazer alterações em seu código, você precisa confirmar se ele realmente funciona. Muitos programadores tentam testar manualmente o básico: a página está sendo renderizada no navegador? O formulário está sendo enviado? O conteúdo correto está sendo exibido? E assim por diante, mas na minha opinião, isso é bárbaro, ineficiente e trabalhoso.
O que eu uso em vez disso
O objetivo de testar um aplicativo da web, seja ele manual ou automatizado, é confirmar que uma determinada página é renderizada no navegador do usuário sem erros fatais e que mostra seu conteúdo corretamente. Uma maneira (e na maioria dos casos, uma maneira mais fácil) de conseguir isso é enviar solicitações HTTP para os terminais do aplicativo e analisar a resposta. O código de resposta informa se a página foi entregue com sucesso. É fácil testar o conteúdo analisando o corpo da resposta da solicitação HTTP e procurando correspondências de strings de texto específicas, ou você pode ser um pouco mais sofisticado e usar bibliotecas de web scraping, como nokogiri.
Se alguns endpoints exigirem um login de usuário, você poderá usar bibliotecas projetadas para automatizar interações (ideal ao fazer testes de integração), como mecanizar para fazer login ou clicar em determinados links. Realmente, no quadro geral dos testes automatizados, isso se parece muito com integração ou teste funcional (dependendo de como você os usa), mas é muito mais rápido de escrever e pode ser incluído em um projeto existente ou adicionado a um novo , com menos esforço do que configurar toda a estrutura de teste. Ponto!
Os casos extremos apresentam outro problema ao lidar com grandes bancos de dados com uma ampla faixa de valores; testar se nosso aplicativo está funcionando sem problemas em todos os conjuntos de dados previstos pode ser assustador.

Uma maneira de fazer isso é antecipar todos os casos extremos (o que não é apenas difícil, muitas vezes é impossível) e escrever um teste para cada um. Isso pode facilmente se tornar centenas de linhas de código (imagine o horror) e complicado de manter. No entanto, com solicitações HTTP e apenas uma linha de código, você pode testar esses casos de borda diretamente nos dados da produção, baixados localmente em sua máquina de desenvolvimento ou em um servidor de teste.
Agora, é claro, essa técnica de teste não é uma bala de prata e tem muitas deficiências, assim como qualquer outro método, mas acho esses tipos de testes mais rápidos e fáceis de escrever e modificar.
Na prática: testando com solicitações HTTP
Como já estabelecemos que escrever código sem qualquer tipo de teste de acompanhamento não é uma boa ideia, meu teste básico para um aplicativo inteiro é enviar solicitações HTTP para todas as suas páginas localmente e analisar os cabeçalhos de resposta para um 200
(ou desejado).
Por exemplo, se fôssemos escrever os testes acima (aqueles que procuram conteúdo específico e um erro fatal) com uma solicitação HTTP (em Ruby), seria algo assim:
# testing for fatal error http_code = `curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) }` if http_code !~ /200/ return “articles_url returned with #{http_code} http code.” end # testing for content active_user = create(:user, name: “user1”, active: true) non_active_user = create(:user, name: “user2”, active: false) content = `curl #{Rails.application.routes.url_helpers.active_user_url(host: 'localhost', port: 3000) }` if content !~ /#{active_user.name}/ return “Content mismatch active user #{active_user.name} not found in text body” #You can customise message to your liking end if content =~ /#{non_active_user.name}/ return “Content mismatch non active user #{active_user.name} found in text body” #You can customise message to your liking end
A linha curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) }
cobre muitos casos de teste; qualquer método que gere um erro na página do artigo será capturado aqui, portanto, abrange efetivamente centenas de linhas de código em um teste.
A segunda parte, que detecta especificamente o erro de conteúdo, pode ser usada várias vezes para verificar o conteúdo de uma página. (Solicitações mais complexas podem ser tratadas usando mechanize
, mas isso está além do escopo deste blog.)
Agora, nos casos em que você deseja testar se uma página específica funciona em um conjunto grande e variado de valores de banco de dados (por exemplo, seu modelo de página de artigo está funcionando para todos os artigos no banco de dados de produção), você pode fazer:
ids = Article.all.select { |post| `curl -s -o /dev/null -w “%{http_code}” #{Rails.application.routes.url_helpers.article_url(post, host: 'localhost', port: 3000) }`.to_i != 200).map(&:id) return ids
Isso retornará uma matriz de IDs de todos os artigos no banco de dados que não foram renderizados, então agora você pode ir manualmente para a página do artigo específico e verificar o problema.
Agora, entendo que essa forma de teste pode não funcionar em certos casos, como testar um script autônomo ou enviar um e-mail, e é inegavelmente mais lento que os testes de unidade porque estamos fazendo chamadas diretas para um endpoint para cada teste, mas quando você não pode ter testes de unidade, ou testes funcionais, ou ambos, isso é melhor que nada.
Como você estruturaria esses testes? Com projetos pequenos e não complexos, você pode escrever todos os seus testes em um arquivo e executar esse arquivo sempre antes de confirmar suas alterações, mas a maioria dos projetos exigirá um conjunto de testes.
Geralmente escrevo de dois a três testes por endpoint, dependendo do que estou testando. Você também pode tentar testar conteúdo individual (semelhante ao teste de unidade), mas acho que seria redundante e lento, pois você fará uma chamada HTTP para cada unidade. Mas, por outro lado, eles serão mais limpos e fáceis de entender.
Eu recomendo colocar seus testes em sua pasta de teste regular com cada ponto final principal tendo seu próprio arquivo (no Rails, por exemplo, cada modelo/controlador teria um arquivo cada), e este arquivo pode ser dividido em três partes de acordo com o que nós estão testando. Costumo ter pelo menos três testes:
Teste um
Verifique se a página retorna sem erros fatais.
Observe como fiz uma lista de todos os endpoints para Post
e iterei sobre ela para verificar se cada página é renderizada sem nenhum erro. Assumindo que tudo correu bem e todas as páginas foram renderizadas, você verá algo assim no terminal: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- []
Se alguma página não for renderizada, você verá algo assim (neste exemplo, a posts/index page
tem erro e, portanto, não é renderizada): ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- [{:url=>”posts_url”, :params=>[], :method=>”GET”, :http_code=>”500”}]
Teste Dois
Confirme se todo o conteúdo esperado está lá:
Se todo o conteúdo que esperamos for encontrado na página, o resultado será assim (neste exemplo, garantimos que posts/:id
tenha um título de postagem, uma descrição e um status): ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of content(s) not found on Post#show page with post id: 1 -- []
Se algum conteúdo esperado não for encontrado na página (aqui esperamos que a página mostre o status da postagem - 'Ativa' se a postagem estiver ativa, 'Desativada' se a postagem estiver desabilitada) o resultado será assim: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of content(s) not found on Post#show page with post id: 1 -- [“Active”]
Teste Três
Verifique se a página é renderizada em todos os conjuntos de dados (se houver):
Se todas as páginas forem renderizadas sem nenhum erro, obteremos uma lista vazia: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error in rendering -- []
Se o conteúdo de alguns dos registros tiver um problema de renderização (neste exemplo, as páginas com o ID 2 e 5 estão dando erro) o resultado será assim: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error on rendering -- [2,5]
Se você quiser brincar com o código de demonstração acima, aqui está meu projeto github.
Então, qual é melhor? Depende…
O teste de solicitação HTTP pode ser sua melhor aposta se:
- Você está trabalhando com um aplicativo da Web
- Você está com pressa e quer escrever algo rápido
- Você está trabalhando com um grande projeto, um projeto pré-existente onde os testes não foram escritos, mas você ainda quer alguma forma de verificar o código
- Seu código envolve solicitação e resposta simples
- Você não quer gastar uma grande parte do seu tempo mantendo testes (li em algum lugar teste unitário = inferno da manutenção, e concordo parcialmente com ele)
- Você deseja testar se um aplicativo funciona em todos os valores em um banco de dados existente
O teste tradicional é ideal quando:
- Você está lidando com algo diferente de um aplicativo da Web, como scripts
- Você está escrevendo um código algorítmico complexo
- Você tem tempo e orçamento para se dedicar a escrever testes
- O negócio requer uma taxa de erros livre de bugs ou baixa (finanças, grande base de usuários)
Obrigado por ler o artigo; agora você deve ter um método de teste que você pode usar como padrão, um com o qual você pode contar quando estiver com pouco tempo.
- Desempenho e eficiência: trabalhando com HTTP/3
- Mantenha-o criptografado, mantenha-o seguro: trabalhando com ESNI, DoH e DoT