Métodos de Teste do `test::nginx`: Configuração, Envio de Solicitações e Tratamento de Respostas

API7.ai

November 18, 2022

OpenResty (NGINX + Lua)

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.