Obstáculo na Contribuição de Código: `test::nginx`

API7.ai

November 17, 2022

OpenResty (NGINX + Lua)

Testar é uma parte essencial do desenvolvimento de software. O conceito de Desenvolvimento Orientado a Testes (TDD, do inglês Test Driven Development) tornou-se tão popular que quase todas as empresas de software possuem uma equipe de QA (Quality Assurance) para cuidar do trabalho de testes.

Testes são a base da qualidade e da grande reputação do OpenResty, mas também são a parte mais negligenciada dos projetos de código aberto do OpenResty. Muitos desenvolvedores usam o lua-nginx-module diariamente e ocasionalmente executam um gráfico de chamas (flame graph), mas quantas pessoas executam os casos de teste? Até mesmo muitos projetos de código aberto baseados no OpenResty não possuem casos de teste. Mas um projeto de código aberto sem casos de teste e integração contínua não é confiável.

No entanto, ao contrário das empresas comerciais, na maioria dos projetos de código aberto não há engenheiros de testes de software dedicados. Então, como eles garantem a qualidade do código? A resposta é simples: "automação de testes" e "integração contínua", com os pontos-chave sendo automação e continuidade, ambos os quais o OpenResty alcançou ao máximo.

O OpenResty possui 70 projetos de código aberto, e seus testes unitários, testes de integração, testes de desempenho, testes de simulação (mock testing), testes de fuzzing (fuzz testing) e outras cargas de trabalho são desafiadores de resolver manualmente apenas pelos contribuidores da comunidade. Portanto, o OpenResty investiu mais em testes automatizados desde o início. Isso pode parecer desacelerar o projeto a curto prazo, mas pode-se dizer que o investimento nessa área é muito rentável a longo prazo. Então, quando converso com outros engenheiros sobre a lógica e o conjunto de ferramentas de testes do OpenResty, eles ficam impressionados.

Vamos falar sobre a filosofia de testes do OpenResty.

Conceito

test::nginx é o núcleo da arquitetura de testes do OpenResty, que é usado pelo próprio OpenResty e pelas bibliotecas lua-resty ao redor para organizar e escrever conjuntos de testes. É uma estrutura de testes com um limiar muito alto. A razão é que, ao contrário das estruturas de testes comuns, o test::nginx não é baseado em asserções e não usa a linguagem Lua, o que exige que os desenvolvedores aprendam e usem o test::nginx do zero e revertam seu conhecimento inerente sobre estruturas de testes.

Conheço vários contribuidores do OpenResty que podem enviar código C e Lua para o OpenResty, mas acham difícil escrever casos de teste usando o test::nginx. Eles ou não sabiam como escrevê-los ou como corrigi-los ao encontrar falhas nos testes. Portanto, chamo o test::nginx de um obstáculo na contribuição de código.

O test::nginx combina Perl, dados orientados e DSL (Domain-specific language). Para o mesmo conjunto de casos de teste, ao controlar os parâmetros e variáveis de ambiente, você pode alcançar diferentes efeitos, como execução aleatória, múltiplas repetições, detecção de vazamentos de memória, testes de estresse, etc.

Instalação e exemplos

Antes de usarmos o test::nginx, vamos aprender como instalá-lo.

Quanto à instalação de software no sistema OpenResty, apenas o método de instalação oficial do CI é o mais oportuno e eficaz; outras formas de instalação sempre encontram vários problemas. É por isso que recomendo que você tome os métodos oficiais como referência, onde você pode encontrar a instalação e o uso do test::nginx também. Há quatro etapas.

  1. Primeiro, instale o gerenciador de pacotes do Perl, cpanminus.
  2. Em seguida, instale o test::nginx via cpanm.
sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1)
  1. Depois, clone o código-fonte mais recente.
git clone https://github.com/openresty/test-nginx.git
  1. Por fim, carregue a biblioteca test-nginx via o comando prove do Perl e execute o conjunto de casos de teste no diretório /t.
prove -Itest-nginx/lib -r t

Após a instalação, vamos ver o caso de teste mais simples no test::nginx. O código a seguir foi adaptado da documentação oficial, e removi todos os parâmetros de controle personalizados.

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


run_tests();

__DATA__

=== TEST 1: set Server
--- config
    location /foo {
        echo hi;
        more_set_headers 'Server: Foo';
    }
--- request
    GET /foo
--- response_headers
Server: Foo
--- response_body
hi

Embora o test::nginx seja escrito em Perl e funcione como um dos módulos, você consegue ver algo em Perl ou em qualquer outra linguagem no teste acima? Isso mesmo. É porque o test::nginx é a própria implementação de DSL do autor em Perl, abstraída especificamente para testar o NGINX e o OpenResty.

Então, quando vemos esse tipo de teste pela primeira vez, provavelmente não entendemos. Mas não se preocupe; vamos analisar o caso de teste acima.

Primeiro, use Test::Nginx::Socket;, que é a forma como o Perl referencia bibliotecas, assim como o require no Lua. Isso também nos lembra que o test::nginx é um programa Perl.

A segunda linha, run_tests();, é uma função Perl no test::nginx, a função de entrada da estrutura de testes. Se você quiser chamar qualquer outra função Perl no test::nginx, elas devem ser colocadas antes de run_tests para serem válidas.

O __DATA__ na terceira linha é um marcador que indica que tudo abaixo dele são dados de teste, e as funções Perl devem ser concluídas antes desse marcador.

O próximo === TEST 1: set Server, o título do caso de teste, indica o propósito deste teste, e ele possui uma ferramenta que automaticamente organiza a numeração internamente.

--- config é o campo de configuração do NGINX. No caso acima, usamos comandos do NGINX, não Lua, e se você quiser adicionar código Lua, fará isso aqui com uma diretiva como content_by_lua.

--- request é usado para simular um terminal enviando uma solicitação, seguido por GET /foo, que especifica o método e o URI da solicitação.

--- response_headers, que é usado para detectar cabeçalhos de resposta. O seguinte Server: Foo indica o header e o value que devem aparecer nos cabeçalhos de resposta. Caso contrário, o teste falhará.

O último, --- response_body, é usado para detectar o corpo da resposta. O seguinte hi é a string que deve aparecer no corpo da resposta; caso contrário, o teste falhará.

Bem, aqui, o caso de teste mais simples foi analisado. Então, entender o caso de teste é um pré-requisito para concluir o trabalho de desenvolvimento relacionado ao OpenResty.

Escreva seus casos de teste

Agora, é hora de colocar a mão na massa. Lembra como testamos o servidor Memcached no último artigo? Isso mesmo; usamos o resty para enviar a solicitação manualmente, o que é representado pelo seguinte código.

resty -e 'local memcached = require "resty.memcached"
    local memc, err = memcached:new()

    memc:set_timeout(1000) -- 1 sec
    local ok, err = memc:connect("127.0.0.1", 11212)
    local ok, err = memc:set("dog", 32)
    if not ok then
        ngx.say("failed to set dog: ", err)
        return
    end

    local res, flags, err = memc:get("dog")
    ngx.say("dog: ", res)'

Mas enviar manualmente não é muito inteligente, certo? Não se preocupe. Podemos tentar transformar testes manuais em automatizados após aprender o test::nginx. Por exemplo:

use Test::Nginx::Socket::Lua::Stream;

run_tests();

__DATA__
  
=== TEST 1: basic get and set
--- config
        location /test {
            content_by_lua_block {
                local memcached = require "resty.memcached"
                local memc, err = memcached:new()
                if not memc then
                    ngx.say("failed to instantiate memc: ", err)
                    return
                end

                memc:set_timeout(1000) -- 1 sec
                local ok, err = memc:connect("127.0.0.1", 11212)

                local ok, err = memc:set("dog", 32)
                if not ok then
                    ngx.say("failed to set dog: ", err)
                    return
                end

                local res, flags, err = memc:get("dog")
                ngx.say("dog: ", res)
            }
        }

--- stream_config
    lua_shared_dict memcached 100m;

--- stream_server_config
    listen 11212;
    content_by_lua_block {
        local m = require("memcached-server")
        m.go()
    }

--- request
GET /test
--- response_body
dog: 32
--- no_error_log
[error]

Neste caso de teste, adicionei --- stream_config, --- stream_server_config, --- no_error_log como itens de configuração, mas eles são essencialmente os mesmos, ou seja,

Os dados e testes dos testes são simplificados para melhorar a legibilidade e a extensibilidade, abstraindo a configuração.

É aqui que o test::nginx é fundamentalmente diferente de outras estruturas de testes. Essa DSL é uma faca de dois gumes, pois torna a lógica de teste clara e facilmente extensível. No entanto, aumenta o custo de aprendizado, exigindo que você reaprenda uma nova sintaxe e configuração antes de começar a escrever casos de teste.

Resumo

O test::nginx é poderoso, mas muitas vezes pode não ser adequado para o seu cenário. Por que usar um canhão para matar uma mosca? No OpenResty, você também tem a opção de usar a estrutura de testes baseada em asserções busted. O busted combinado com o resty se torna uma ferramenta de linha de comando e também pode atender a muitas necessidades de testes.

Por fim, deixo uma pergunta para você. Você consegue executar esse teste para o Memcached localmente? Se você puder adicionar um novo caso de teste, seria ótimo.