Métodos de Teste do `test::nginx`: Configuração, Envio de Solicitações e Tratamento de Respostas
API7.ai
November 18, 2022
No artigo anterior, já tivemos o primeiro vislumbre do test::nginx
e executamos o exemplo mais simples. No entanto, em um projeto de código aberto real, os casos de teste escritos em test::nginx
são muito mais complexos e difíceis de dominar do que o código de exemplo. Caso contrário, não seria chamado de obstáculo.
Neste artigo, vou guiá-lo pelos comandos e métodos de teste mais frequentemente usados no test::nginx
, para que você possa entender a maioria dos conjuntos de casos de teste no projeto OpenResty e tenha a capacidade de escrever casos de teste mais realistas. Mesmo que você ainda não tenha contribuído com código para o OpenResty, familiarizar-se com o framework de testes do OpenResty será uma grande inspiração para você projetar e escrever casos de teste em seu trabalho.
O teste test::nginx
essencialmente gera um nginx.conf
e inicia um processo NGINX com base na configuração de cada caso de teste. Em seguida, ele simula uma solicitação de cliente com o corpo e os cabeçalhos especificados. Depois, o código Lua no caso de teste processa a solicitação e faz uma resposta. Nesse momento, o test::nginx
analisa informações críticas como o corpo da resposta, os cabeçalhos da resposta e os logs de erro e os compara com a configuração do teste. Se houver uma discrepância, o teste falha com um erro; caso contrário, é bem-sucedido.
O test::nginx
fornece muitos primitivos DSL (Linguagem Específica de Domínio). Fiz uma classificação simples de acordo com a configuração do NGINX, o envio de solicitações, o processamento de respostas e a verificação de logs. Esses 20% da funcionalidade podem cobrir 80% dos cenários de aplicação, por isso devemos ter um domínio firme sobre eles. Quanto a outros primitivos e usos mais avançados, vamos apresentá-los no próximo artigo.
Configuração do NGINX
Vamos primeiro olhar para a configuração do NGINX. O primitivo do test::nginx
com a palavra-chave "config" está relacionado à configuração do NGINX, como config
, stream_config
, http_config
, etc.
Suas funções são as mesmas: inserir a configuração especificada do NGINX em diferentes contextos do NGINX. Essas configurações podem ser comandos do NGINX ou código Lua encapsulado em content_by_lua_block
.
Ao fazer testes de unidade, config
é o primitivo mais comumente usado, no qual carregamos bibliotecas Lua e chamamos funções para testes de caixa branca. Aqui está um trecho de código de teste, que não pode ser executado completamente. Ele é de um projeto de código aberto real, então, se você estiver interessado, pode clicar no link para ver o teste completo, ou pode tentar executá-lo localmente.
=== TEST 1: sanity
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.key-auth")
local ok, err = plugin.check_schema({key = 'test-key'})
if not ok then
ngx.say(err)
end
ngx.say("done")
}
}
O objetivo deste caso de teste é verificar se a função check_schema
no arquivo de código plugins.key-auth
funciona corretamente. Ele usa o comando NGINX content_by_lua_block
em location /t
para requerer o módulo a ser testado e chamar diretamente a função que precisa ser verificada.
Este é um meio comum de teste de caixa branca no test::nginx
. No entanto, essa configuração sozinha não é suficiente para completar o teste, então vamos continuar e ver como enviar uma solicitação de cliente.
Enviando Solicitações
Simular um cliente enviando uma solicitação envolve vários detalhes, então vamos começar com o mais simples - enviar uma única solicitação.
request
Continuando com o caso de teste acima, se quisermos que o código de teste de unidade seja executado, então temos que iniciar uma solicitação HTTP para o endereço /t
especificado na configuração, como mostrado no seguinte código de teste:
--- request
GET /t
Este código envia uma solicitação GET
para /t
no primitivo de solicitação. Aqui, não especificamos o endereço IP, domínio ou porta de acesso, nem especificamos se é HTTP 1.0
ou HTTP 1.1
. Todos esses detalhes são ocultados pelo test::nginx
, então não precisamos nos preocupar. Este é um dos benefícios do DSL - precisamos apenas nos concentrar na lógica de negócios sem nos distrair com todos os detalhes.
Além disso, isso fornece flexibilidade parcial. Por exemplo, o padrão é o protocolo para HTTP 1.1
, ou se quisermos testar HTTP 1.0
, podemos especificar separadamente:
--- request
GET /t HTTP/1.0
Além do método GET
, o método POST
também precisa ser suportado. No exemplo a seguir, podemos POST
a string hello world
para o endereço especificado.
--- request
POST /t
hello world
Novamente, o test::nginx
calcula o comprimento do corpo da solicitação para você aqui e adiciona os cabeçalhos de solicitação host
e connection
para garantir que esta seja uma solicitação normal automaticamente.
Claro, podemos adicionar comentários para torná-lo mais legível. Aqueles que começam com #
serão reconhecidos como comentários de código.
--- request
# post request
POST /t
hello world
A solicitação também suporta um modo mais complexo e flexível, que usa eval
como um filtro para incorporar código Perl diretamente, já que o test::nginx
é escrito em Perl. Se a linguagem DSL atual não atender às suas necessidades, eval
é a "arma definitiva" para executar código Perl diretamente.
Para o uso de eval
, vamos ver alguns exemplos simples aqui, e continuaremos com outros mais complexos no próximo artigo.
--- request eval
"POST /t
hello\x00\x01\x02
world\x03\x04\xff"
No primeiro exemplo, usamos eval
para especificar caracteres não imprimíveis, que é um de seus usos. O conteúdo entre as aspas duplas será tratado como uma string Perl e então passado para o request
como um argumento.
Aqui está um exemplo mais interessante:
--- request eval
"POST /t\n" . "a" x 1024
No entanto, para entender este exemplo, precisamos saber algo sobre strings em Perl, então preciso mencionar brevemente dois pontos aqui.
- Em Perl, usamos um ponto para representar a concatenação de strings. Isso não é um pouco semelhante aos dois pontos do
Lua
? - Um
x
minúsculo indica o número de vezes que um caractere é repetido. Por exemplo, o"a" x 1024
acima significa que o caractere "a" é repetido 1024 vezes.
Portanto, o segundo exemplo significa que o método POST
envia uma solicitação contendo 1024
caracteres a
para o endereço /t
.
pipelined_requests
Depois de entender como enviar uma única solicitação, vamos ver como enviar várias solicitações. No test::nginx
, podemos usar o primitivo pipelined_requests
para enviar várias solicitações em sequência dentro da mesma conexão keep-alive
:
--- pipelined_requests eval
["GET /hello", "GET /world", "GET /foo", "GET /bar"]
Por exemplo, este exemplo acessará essas quatro APIs sequencialmente na mesma conexão. Há duas vantagens sobre isso:
- A primeira é que muito código de teste repetitivo pode ser eliminado, e os quatro casos de teste podem ser comprimidos em um.
- A segunda e mais importante razão é que podemos usar solicitações em pipeline para detectar se a lógica do código terá exceções no caso de múltiplos acessos.
Você pode se perguntar, se eu escrever vários casos de teste em sequência, então o código também será executado várias vezes na fase de execução. Isso não cobre o segundo problema acima?
Isso se resume ao modo de execução do test::nginx
, que funciona de maneira diferente do que você pode pensar. Após cada caso de teste, o test::nginx
encerra o processo NGINX atual, e todos os dados na memória desaparecem. Ao executar o próximo caso de teste, o nginx.conf
é regenerado, e um novo Worker
do NGINX é iniciado. Esse mecanismo garante que os casos de teste não se afetem.
Portanto, quando queremos testar várias solicitações, precisamos usar o primitivo pipelined_requests
. Com base nele, podemos simular limitação de taxa, limitação de concorrência e muitos outros cenários para testar se o seu sistema funciona corretamente com cenários mais realistas e complexos. Vamos deixar isso para o próximo artigo também, pois envolverá vários comandos e primitivos.
repeat_each
Acabamos de mencionar o caso de testar várias solicitações, então como devemos executar o mesmo teste várias vezes?
Para esse problema, o test::nginx
fornece uma configuração global: repeat_each
, que é uma função Perl que por padrão é repeat_each(1)
, indicando que o caso de teste será executado apenas uma vez. Portanto, nos casos de teste anteriores, não nos preocupamos em configurá-lo separadamente.
Naturalmente, podemos configurá-lo antes da função run_test()
, por exemplo, alterando o argumento para 2
.
repeat_each(2);
run_tests();
Então, cada caso de teste é executado duas vezes, e assim por diante.
more_headers
Depois de falar sobre o corpo da solicitação, vamos olhar para os cabeçalhos da solicitação. Como mencionamos acima, o test::nginx
envia a solicitação com os cabeçalhos host
e connection
por padrão. E os outros cabeçalhos da solicitação?
more_headers
é especificamente projetado para fazer isso.
--- more_headers
X-Foo: blah
Podemos usá-lo para definir vários cabeçalhos personalizados. Se quisermos definir mais de um cabeçalho, então definimos mais de uma linha:
--- more_headers
X-Foo: 3
User-Agent: openresty
Processando Respostas
Depois de enviar a solicitação, a parte mais importante do test::nginx
é processar a resposta, onde determinaremos se a resposta atende às expectativas. Aqui dividimos isso em quatro partes e as apresentamos: o corpo da resposta, o cabeçalho da resposta, o código de status da resposta e o log.
response_body
O contraparte do primitivo de solicitação é response_body
, e o seguinte é um exemplo de suas duas configurações em uso:
=== TEST 1: sanity
--- config
location /t {
content_by_lua_block {
ngx.say("hello")
}
}
--- request
GET /t
--- response_body
hello
Este caso de teste passará se o corpo da resposta for hello
, e reportará um erro em outros casos. Mas como testamos um corpo de retorno longo? Não se preocupe, o test::nginx
já cuidou disso para você. Ele suporta a detecção do corpo da resposta com uma expressão regular, como a seguinte:
--- response_body_like
^he\w+$
Isso permite que você seja muito flexível com o corpo da resposta. Além disso, o test::nginx
também suporta operações unlike
:
--- response_body_unlike
^he\w+$
Neste ponto, se o corpo da resposta for hello
, o teste não passará.
Na mesma linha, após entender a detecção de uma única solicitação, vamos olhar para a detecção de várias solicitações. Aqui está um exemplo de como usá-lo com pipelined_requests
:
--- pipelined_requests eval
["GET /hello", "GET /world", "GET /foo", "GET /bar"]
--- response_body eval
["hello", "world", "oo", "bar"]
Claro, o importante a notar aqui é que, quantas solicitações você enviar, você precisará ter tantas respostas para corresponder.
response_headers
Em segundo lugar, vamos falar sobre o cabeçalho da resposta. O cabeçalho da resposta é semelhante ao cabeçalho da solicitação, onde cada linha corresponde à chave e ao valor de um cabeçalho.
--- response_headers
X-RateLimit-Limit: 2
X-RateLimit-Remaining: 1
Como a detecção do corpo da resposta, os cabeçalhos da resposta também suportam expressões regulares e operações unlike
, como response_headers_like
, raw_response_headers_like
e raw_response_headers_unlike
.
error_code
O terceiro é o código de resposta. A detecção do código de resposta suporta comparação direta e também suporta operações like
, como os dois exemplos a seguir:
--- error_code: 302
--- error_code_like: ^(?:500)?$
No caso de várias solicitações, o error_code
precisa ser verificado várias vezes:
--- pipelined_requests eval
["GET /hello", "GET /hello", "GET /hello", "GET /hello"]
--- error_code eval
[200, 200, 503, 503]
error_log
O último item de teste é o log de erro. Na maioria dos casos de teste, nenhum log de erro é gerado. Podemos usar no_error_log
para detectar:
--- no_error_log
[error]
No exemplo acima, se a string [error]
aparecer no error.log
do NGINX, o teste falhará. Este é um recurso muito comum, e é recomendável que você adicione a detecção do log de erro a todos os seus testes normais.
--- error_log
hello world
A configuração acima está detectando a presença de hello world
no error.log
. Claro, você pode usar eval
incorporado em código Perl para implementar a detecção de expressão regular, como o seguinte:
--- error_log eval
qr/\[notice\] .*? \d+ hello world/
Resumo
Hoje, estamos aprendendo como enviar solicitações e testar respostas no test::nginx
, contendo o corpo da solicitação, o cabeçalho, o código de status da resposta e o log de erro. Podemos implementar um conjunto completo de casos de teste com a combinação desses primitivos.
Finalmente, aqui está uma questão para reflexão: Quais são as vantagens e desvantagens do test::nginx
, uma DSL abstrata? Sinta-se à vontade para deixar comentários e discutir comigo, e você também é bem-vindo a compartilhar este artigo para comunicar e pensar juntos.