Obstáculo en la Contribución de Código: `test::nginx`

API7.ai

November 17, 2022

OpenResty (NGINX + Lua)

Las pruebas son una parte esencial del desarrollo de software. El concepto de Desarrollo Guiado por Pruebas (TDD, por sus siglas en inglés) se ha vuelto tan popular que casi todas las empresas de software tienen un equipo de QA (Garantía de Calidad) para encargarse del trabajo de pruebas.

Las pruebas son la piedra angular de la calidad y la gran reputación de OpenResty, pero también son la parte más descuidada de los proyectos de código abierto de OpenResty. Muchos desarrolladores usan el lua-nginx-module a diario y ocasionalmente ejecutan un gráfico de llamadas (flame graph), pero ¿cuántas personas ejecutan los casos de prueba? Incluso muchos proyectos de código abierto basados en OpenResty carecen de casos de prueba. Pero un proyecto de código abierto sin casos de prueba e integración continua no es confiable.

Sin embargo, a diferencia de las empresas comerciales, en la mayoría de los proyectos de código abierto no hay ingenieros dedicados a las pruebas de software, entonces, ¿cómo aseguran la calidad de su código? La respuesta es simple: "automatización de pruebas" e "integración continua", siendo los puntos clave la automatización y la continuidad, ambos logrados por OpenResty en la mayor medida posible.

OpenResty tiene 70 proyectos de código abierto, y sus pruebas unitarias, de integración, de rendimiento, de simulación (mock), de fuzz y otras cargas de trabajo son difíciles de resolver manualmente por los contribuyentes de la comunidad. Por lo tanto, OpenResty invirtió más en pruebas automatizadas desde el principio. Esto puede parecer que ralentiza el proyecto a corto plazo, pero se puede decir que la inversión en esta área es muy rentable a largo plazo. Así que cuando hablo con otros ingenieros sobre la lógica y el conjunto de herramientas de pruebas de OpenResty, se quedan asombrados.

Hablemos de la filosofía de pruebas de OpenResty.

Concepto

test::nginx es el núcleo de la arquitectura de pruebas de OpenResty, que es utilizada por OpenResty mismo y las bibliotecas circundantes lua-resty para organizar y escribir conjuntos de pruebas. Es un marco de pruebas con un umbral muy alto. La razón es que, a diferencia de los marcos de pruebas comunes, test::nginx no se basa en aserciones y no utiliza el lenguaje Lua, lo que requiere que los desarrolladores aprendan y usen test::nginx desde cero y reviertan su conocimiento inherente de los marcos de pruebas.

Conozco a varios contribuyentes de OpenResty que pueden enviar código C y Lua a OpenResty, pero sienten que es difícil escribir casos de prueba usando test::nginx. O no sabían cómo escribirlos o cómo solucionarlos cuando encontraban fallos en las pruebas. Por lo tanto, llamo a test::nginx un obstáculo en la contribución de código.

test::nginx combina Perl, datos impulsados y DSL (Lenguaje específico del dominio). Para el mismo conjunto de casos de prueba, al controlar los parámetros y las variables de entorno, se pueden lograr diferentes efectos como ejecución aleatoria, múltiples repeticiones, detección de fugas de memoria, pruebas de estrés, etc.

Instalación y ejemplos

Antes de usar test::nginx, aprendamos cómo instalarlo.

En cuanto a la instalación de software en el sistema OpenResty, solo el método de instalación oficial de CI es el más oportuno y efectivo; otras formas de instalación siempre encuentran varios problemas. Por eso recomiendo que tomen los métodos oficiales como referencia, donde también pueden encontrar la instalación y el uso de test::nginx. Hay cuatro pasos.

  1. Primero, instale el gestor de paquetes de Perl cpanminus.
  2. Luego, instale test::nginx a través de cpanm.
sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1)
  1. A continuación, clone el código fuente más reciente.
git clone https://github.com/openresty/test-nginx.git
  1. Finalmente, cargue la biblioteca test-nginx a través del comando prove de Perl y ejecute el conjunto de casos de prueba en el directorio /t.
prove -Itest-nginx/lib -r t

Después de la instalación, veamos el caso de prueba más simple en test::nginx. El siguiente código está adaptado de la documentación oficial, y he eliminado todos los parámetros de control 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

Aunque test::nginx está escrito en Perl y funciona como uno de sus módulos, ¿puedes ver algo en Perl o en cualquier otro lenguaje en la prueba anterior? Así es. Esto se debe a que test::nginx es la propia implementación de DSL del autor en Perl, abstraída específicamente para probar NGINX y OpenResty.

Por lo tanto, cuando vemos este tipo de prueba por primera vez, es muy probable que no la entendamos. Pero no te preocupes; analicemos el caso de prueba anterior.

En primer lugar, use Test::Nginx::Socket;, que es la forma en que Perl hace referencia a las bibliotecas, al igual que require en Lua. Esto también nos recuerda que test::nginx es un programa en Perl.

La segunda línea, run_tests();, es una función en Perl de test::nginx, la función de entrada para el marco de pruebas. Si deseas llamar a cualquier otra función en Perl de test::nginx, deben colocarse antes de run_tests para que sean válidas.

El __DATA__ en la tercera línea es una bandera que indica que todo lo que está debajo son datos de prueba, y las funciones de Perl deben completarse antes de esta bandera.

El siguiente === TEST 1: set Server, el título del caso de prueba, indica el propósito de esta prueba, y tiene una herramienta que automáticamente asigna la numeración interna.

--- config es el campo de configuración de NGINX. En el caso anterior, usamos comandos de NGINX, no Lua, y si deseas agregar código Lua, lo harás aquí con una directiva como content_by_lua.

--- request se usa para simular un terminal que envía una solicitud, seguido de GET /foo, que especifica el método y el URI de la solicitud.

--- response_headers, que se usa para detectar los encabezados de respuesta. El siguiente Server: Foo indica el header y value que deben aparecer en los encabezados de respuesta. Si no es así, la prueba fallará.

El último, --- response_body, se usa para detectar el cuerpo de la respuesta. El siguiente hi es la cadena que debe aparecer en el cuerpo de la respuesta; si no lo hace, la prueba fallará.

Bueno, aquí termina el análisis del caso de prueba más simple. Entonces, entender el caso de prueba es un requisito previo para completar el trabajo de desarrollo relacionado con OpenResty.

Escribe tus casos de prueba

A continuación, es hora de entrar en las pruebas prácticas. ¿Recuerdas cómo probamos el servidor Memcached en el último artículo? Así es; usamos resty para enviar la solicitud manualmente, lo cual está representado por el siguiente 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)'

Pero, ¿no es lo suficientemente inteligente enviarlo manualmente? No te preocupes. Podemos intentar convertir las pruebas manuales en automatizadas después de aprender test::nginx. Por ejemplo:

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]

En este caso de prueba, he agregado --- stream_config, --- stream_server_config, --- no_error_log como elementos de configuración, pero son esencialmente lo mismo, es decir.

Los datos y las pruebas se simplifican para mejorar la legibilidad y la extensibilidad al abstraer la configuración.

Aquí es donde test::nginx es fundamentalmente diferente de otros marcos de pruebas. Este DSL es una espada de doble filo, ya que hace que la lógica de las pruebas sea clara y fácilmente extensible. Sin embargo, aumenta el costo de aprendizaje, requiriendo que aprendas una nueva sintaxis y configuración antes de poder comenzar a escribir casos de prueba.

Resumen

El test::nginx es poderoso, pero muchas veces puede no ser adecuado para tu escenario. ¿Por qué usar un cañón para matar una mosca? En OpenResty, también tienes la opción de usar el marco de pruebas basado en aserciones busted. El busted combinado con resty se convierte en una herramienta de línea de comandos y también puede satisfacer muchas necesidades de pruebas.

Finalmente, te dejo una pregunta. ¿Puedes ejecutar esta prueba para Memcached localmente? Si puedes agregar un nuevo caso de prueba, sería genial.