Uso Pouco Conhecido do `test::nginx`

API7.ai

November 24, 2022

OpenResty (NGINX + Lua)

Nos dois artigos anteriores, você dominou a maior parte do uso do test::nginx, e acredito que você possa entender a maioria dos conjuntos de casos de teste no projeto OpenResty. Isso é mais do que suficiente para aprender OpenResty e suas bibliotecas relacionadas.

Mas se você estiver interessado em se tornar um contribuidor de código do OpenResty, ou se estiver usando test::nginx para escrever casos de teste em seus projetos, então você precisa aprender alguns usos mais avançados e complexos.

O artigo de hoje provavelmente será a parte mais "impopular" da série, porque é algo que ninguém compartilhou antes. Tomando lua-nginx-module, o módulo central do OpenResty, como exemplo, que tem mais de 70 contribuidores em todo o mundo, mas nem todo contribuidor escreveu um caso de teste. Então, se você ler o artigo de hoje, seu entendimento do test::nginx entrará no Top 100 mundial.

Depuração em teste

Primeiro, vamos ver algumas das seções mais simples e comumente usadas que os desenvolvedores usam na depuração normal. Aqui, apresentaremos os cenários de uso dessas seções relacionadas à depuração, uma por uma.

ONLY

Muitas vezes, adicionamos um novo caso de teste ao conjunto original de casos de teste. Se o arquivo de teste contiver muitos casos de teste, é demorado executá-lo, especialmente quando você precisa modificar os casos de teste repetidamente.

Então, existe alguma maneira de executar apenas um dos casos de teste que você especifica? Isso pode ser facilmente feito com a seção ONLY.

=== TEST 1: sanity
=== TEST 2: get
--- ONLY

O pseudocódigo acima mostra como usar essa seção. Ao colocar --- ONLY na última linha do caso de teste que precisa ser executado sozinho, então quando você usar prove para executar o arquivo de caso de teste, todos os outros casos de teste serão ignorados, e apenas este teste será executado.

No entanto, isso só é apropriado quando você está depurando. Então, quando o comando prove encontrar a seção ONLY, ele também alertará para não esquecer de removê-la ao enviar seu código.

SKIP

A necessidade correspondente à execução de apenas um caso de teste é ignorar um caso de teste específico. A seção SKIP, que é normalmente usada para testar funcionalidades que ainda não foram implementadas:

=== TEST 1: sanity
=== TEST 2: get
--- SKIP

Como você pode ver neste pseudocódigo, seu uso é semelhante ao ONLY. Como estamos desenvolvendo com base em testes, precisamos escrever casos de teste primeiro; e quando estamos codificando a implementação coletivamente, podemos precisar adiar a implementação de uma funcionalidade devido à dificuldade ou prioridade de implementação. Então, você pode pular o conjunto de casos de teste correspondente primeiro e, em seguida, remover a seção SKIP quando a implementação estiver concluída.

LAST

Outra seção comum é LAST, que também é simples de usar, pois os casos de teste antes dela serão executados e os depois serão ignorados.

=== TEST 1: sanity
=== TEST 2: get
--- LAST
=== TEST 3: set

Você pode se perguntar, eu entendo a importância de ONLY e SKIP, mas qual é a utilidade de LAST? Na verdade, às vezes seus casos de teste têm dependências, e você precisa executar os primeiros casos de teste antes que os testes subsequentes façam sentido. Então, nesse caso, LAST é muito útil quando você continua a depuração.

plan

De todas as funções do test::nginx, plan é uma das mais frustrantes e difíceis de entender. Ela é derivada do módulo Test::Plan do Perl, cuja documentação não está no test::nginx, e encontrar uma explicação sobre ela não é fácil. Portanto, vou apresentá-la na parte inicial. Já vi vários contribuidores de código do OpenResty que caíram nessa armadilha e nem conseguiram sair.

Aqui está um exemplo de uma configuração semelhante que você pode ver no início de cada arquivo no conjunto de testes oficial do OpenResty:

plan tests => repeat_each() * (3 * blocks());

O significado de plan aqui é quantos testes devem ser feitos de acordo com o plano em todo o arquivo de teste. Se o resultado da execução final não corresponder ao plano, o teste falhará.

Para este exemplo, se o valor de repeat_each for 2 e houver 10 casos de teste, então o valor de plan deve ser 2 x 3 x 10 = 60. A única coisa que pode causar confusão é o significado do número 3, que parece um número mágico!

Não se preocupe, vamos continuar olhando o exemplo, você conseguirá entender em um momento. Primeiro, você consegue descobrir qual é o valor correto de plan no seguinte caso de teste?

=== TEST 1: sanity
--- config
    location /t {
        content_by_lua_block {
            ngx.say("hello")
        }
    }
--- request
GET /t
--- response_body
hello

Acredito que todos concluiriam que plan = 1, já que o teste só verifica o response_body.

Mas esse não é o caso! A resposta correta é que plan = 2. Por quê? Porque test::nginx tem uma verificação implícita, ou seja, --- error_code: 200, que detecta se o código de resposta HTTP é 200 por padrão.

Então, o número mágico 3 acima realmente significa que cada teste é explicitamente verificado duas vezes, por exemplo, para body e error log, e implicitamente para response code.

Como isso é tão propenso a erros, recomendo que você desative o plan usando o seguinte método.

use Test::Nginx::Socket 'no_plan';

Se você não puder desativá-lo, por exemplo, se encontrar um plan impreciso no conjunto de testes oficial do OpenResty, é recomendável que você não se aprofunde na causa, mas simplesmente adicione ou subtraia números nas expressões do plan.

plan tests => repeat_each() * (3 * blocks()) + 2;

Este também é o método oficial que será usado.

Pré-processador

Sabemos que pode haver algumas configurações públicas entre diferentes casos de teste do mesmo arquivo de teste. Se as configurações forem repetidas em cada caso de teste, isso tornará o código redundante e complicado de modificar posteriormente.

Nesse ponto, você pode usar a diretiva add_block_preprocessor para adicionar um trecho de código Perl, como o seguinte:

add_block_preprocessor(sub {
    my $block = shift;

    if (!defined $block->config) {
        $block->set_value("config", <<'_END_');
    location = /t {
        echo $arg_a;
    }
    _END_
    }
});

Este pré-processador adiciona uma seção config a todos os casos de teste, e o conteúdo é location /t, para que, em seus casos de teste posteriores, você possa omitir o config e acessá-lo diretamente.

=== TEST 1:
--- request
    GET /t?a=3
--- response_body
3

=== TEST 2:
--- request
    GET /t?a=blah
--- response_body
blah

Funções Personalizadas

Além de adicionar código Perl ao pré-processador, você também pode adicionar arbitrariamente funções Perl, ou funções personalizadas como as chamamos, antes da função run_tests.

Aqui está um exemplo que adiciona uma função que lê um arquivo e a combina com a diretiva eval para implementar um POST de arquivo:

sub read_file {
    my $infile = shift;
    open my $in, $infile
        or die "cannot open $infile for reading: $!";
    my $content = do { local $/; <$in> };
    close $in;
    $content;
}

our $CONTENT = read_file("t/test.jpg");

run_tests;

__DATA__

=== TEST 1: sanity
--- request eval
"POST /\n$::CONTENT"

Embaralhamento

Além do acima, test::nginx tem uma armadilha pouco conhecida: ele executa casos de teste em ordem aleatória por padrão, em vez de seguir a ordem e a numeração dos casos de teste.

Isso foi inicialmente planejado para testar mais problemas. Afinal, após cada caso de teste ser executado, o processo NGINX é fechado, e um novo processo NGINX é iniciado para executá-lo, então os resultados não devem estar relacionados à ordem.

Para projetos de nível básico, isso é verdade. No entanto, para projetos de nível de aplicação, existe armazenamento persistente, como bancos de dados, externamente. Uma execução desordenada pode levar a resultados errados. Como é aleatório a cada vez, pode ou não relatar um erro, e o erro pode ser diferente a cada vez. Isso obviamente causa confusão para os desenvolvedores, incluindo eu, que já tropecei aqui várias vezes.

Então, meu conselho é: por favor, desative esse recurso. Você pode desativá-lo com as seguintes duas linhas de código:

no_shuffle();
run_tests;

Em particular, o no_shuffle é usado para desativar a randomização e permite que os testes sejam executados estritamente na ordem dos casos de teste.

reindex

O conjunto de casos de teste do OpenResty tem requisitos de formatação rigorosos. Cada caso de teste precisa ser separado por três novas linhas, e a numeração dos casos de teste tem que ser estritamente crescente.

Felizmente, temos uma ferramenta automática, reindex, para fazer essas coisas tediosas, que está escondida no projeto openresty-devel-utils. Como não há documentação sobre isso, apenas algumas pessoas sabem disso.

Se você estiver interessado, pode tentar bagunçar a numeração dos casos de teste, ou adicionar ou remover o número de quebras de linha, e então usar essa ferramenta para organizá-los e ver se consegue restaurá-los.

Resumo

Este é o fim da introdução ao test::nginx. Claro, há mais funções, nós só falamos sobre as principais e mais importantes. "Dê um peixe a um homem e você o alimenta por um dia; ensine-o a pescar e você o alimenta por toda a vida." Eu ensinei a você os métodos básicos e as precauções para aprender testes, então você pode se aprofundar no conjunto de casos de teste oficial para um melhor entendimento.

Finalmente, pense nas perguntas abaixo. Há testes no desenvolvimento do seu projeto? E qual framework você usa para testar? Convido você a compartilhar este artigo com mais pessoas para trocar e aprender juntos.