Uso poco conocido de `test::nginx`

API7.ai

November 24, 2022

OpenResty (NGINX + Lua)

En los dos artículos anteriores, has dominado la mayor parte del uso de test::nginx, y creo que puedes entender la mayoría de los conjuntos de casos de prueba en el proyecto OpenResty. Esto es más que suficiente para aprender OpenResty y sus bibliotecas circundantes.

Pero si estás interesado en convertirte en un contribuidor de código de OpenResty, o si estás utilizando test::nginx para escribir casos de prueba en tus proyectos, entonces necesitas aprender algunos usos más avanzados y complejos.

El artículo de hoy probablemente será la parte más "impopular" de la serie porque es algo que nadie ha compartido antes. Tomemos lua-nginx-module, el módulo central en OpenResty, como ejemplo, que tiene más de 70 contribuidores en todo el mundo, pero no todos los contribuidores han escrito un caso de prueba. Así que si lees el artículo de hoy, tu comprensión de test::nginx entrará en el Top 100 mundial.

Depuración en pruebas

Primero, veamos algunas de las secciones más simples y comúnmente utilizadas que los desarrolladores usan en la depuración normal. Aquí, presentaremos los escenarios de uso de estas secciones relacionadas con la depuración una por una.

ONLY

Muy a menudo, añadimos un nuevo caso de prueba al conjunto original de casos de prueba. Si el archivo de prueba contiene muchos casos de prueba, es costoso ejecutarlo, especialmente cuando necesitas modificar los casos de prueba repetidamente.

Entonces, ¿hay alguna manera de ejecutar solo uno de los casos de prueba que especificas? Esto se puede hacer fácilmente con la sección ONLY.

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

El pseudocódigo anterior muestra cómo usar esta sección. Al poner --- ONLY en la última línea del caso de prueba que necesita ejecutarse solo, cuando uses prove para ejecutar el archivo de casos de prueba, todos los otros casos de prueba serán ignorados, y solo se ejecutará esta prueba.

Sin embargo, esto solo es apropiado cuando estás haciendo depuración. Por lo tanto, cuando el comando prove encuentra la sección ONLY, también te recordará que no olvides eliminarla cuando comprometas tu código.

SKIP

El requisito correspondiente a ejecutar solo un caso de prueba es ignorar un caso de prueba particular. La sección SKIP, que se usa típicamente para probar funcionalidades que aún no se han implementado:

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

Como puedes ver en este pseudocódigo, su uso es similar a ONLY. Como estamos desarrollando con pruebas guiadas, necesitamos escribir casos de prueba primero; y cuando estamos codificando la implementación colectivamente, podríamos necesitar retrasar la implementación de una funcionalidad debido a la dificultad o prioridad de implementación. Entonces, puedes omitir el conjunto de casos de prueba correspondiente primero, y luego eliminar la sección SKIP cuando la implementación esté completa.

LAST

Otra sección común es LAST, que también es simple de usar, ya que los casos de prueba anteriores a ella se ejecutarán y los posteriores se ignorarán.

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

Podrías preguntarte, puedo entender la importancia de ONLY y SKIP, pero ¿para qué sirve LAST? De hecho, a veces tus casos de prueba tienen dependencias, y necesitas ejecutar los primeros casos de prueba antes de que las pruebas posteriores tengan sentido. Entonces, en este caso, LAST es muy útil cuando continúas depurando.

plan

De todas las funciones de test::nginx, plan es una de las más frustrantes y difíciles de entender. Se deriva del módulo Test::Plan de Perl, cuya documentación no está en test::nginx, y encontrar una explicación de ello no es fácil. Por lo tanto, lo presentaré en la parte inicial. He visto a varios contribuidores de código de OpenResty que han caído en este agujero y ni siquiera han podido salir.

Aquí hay un ejemplo de una configuración similar que puedes ver al principio de cada archivo en el conjunto de pruebas oficial de OpenResty:

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

El significado de plan aquí es cuántas pruebas se deben hacer según el plan en todo el archivo de prueba. Si el resultado de la ejecución final no coincide con el plan, la prueba fallará.

Para este ejemplo, si el valor de repeat_each es 2 y hay 10 casos de prueba, entonces el valor de plan debería ser 2 x 3 x 10 = 60. Lo único que puede confundirte es el significado del número 3, ¡que parece un número mágico!

No te preocupes, sigamos mirando el ejemplo, podrás entenderlo en un momento. Primero, ¿puedes averiguar cuál es el valor correcto de plan en el siguiente caso de prueba?

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

Creo que todos concluirían que plan = 1, ya que la prueba solo verifica el response_body.

¡Pero ese no es el caso! La respuesta correcta es que plan = 2. ¿Por qué? Porque test::nginx tiene una verificación implícita, es decir, --- error_code: 200, que detecta si el código de respuesta HTTP es 200 por defecto.

Entonces, el número mágico 3 realmente significa que cada prueba se verifica explícitamente dos veces, por ejemplo para body y error log, e implícitamente para response code.

Como esto es tan propenso a errores, te recomiendo que desactives plan usando el siguiente método.

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

Si no puedes desactivarlo, por ejemplo, si encuentras un plan inexacto en el conjunto de pruebas oficial de OpenResty, se recomienda que no profundices en la causa, sino que simplemente sumes o restes números a las expresiones de plan.

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

Este también es el método oficial que se utilizará.

Preprocesador

Sabemos que puede haber algunas configuraciones públicas entre diferentes casos de prueba del mismo archivo de prueba. Si las configuraciones se repiten en cada caso de prueba, hará que el código sea redundante y molesto de modificar más tarde.

En este punto, puedes usar la directiva add_block_preprocessor para añadir un fragmento de código Perl, como el siguiente:

add_block_preprocessor(sub {
    my $block = shift;

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

Este preprocesador añade una sección config a todos los casos de prueba, y el contenido es location /t, de modo que en tus casos de prueba posteriores, puedes omitir el config y acceder directamente.

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

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

Funciones Personalizadas

Además de añadir código Perl al preprocesador, también puedes añadir arbitrariamente funciones Perl, o funciones personalizadas como las llamamos, antes de la función run_tests.

Aquí hay un ejemplo que añade una función que lee un archivo y lo combina con la directiva eval para implementar un POST de archivo:

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"

Aleatorización

Además de lo anterior, test::nginx tiene una trampa poco conocida: ejecuta los casos de prueba en orden aleatorio por defecto, en lugar de seguir el orden y la numeración de los casos de prueba.

Inicialmente se pretendía probar más problemas. Después de todo, después de que se ejecuta cada caso de prueba, el proceso NGINX se cierra, y se inicia un nuevo proceso NGINX para ejecutarlo, por lo que los resultados no deberían estar relacionados con el orden.

Para proyectos de nivel inferior, esto es cierto. Sin embargo, para proyectos de nivel de aplicación, existe almacenamiento persistente como bases de datos externamente. Una ejecución desordenada puede llevar a resultados incorrectos. Como es aleatorio cada vez, puede o no reportar un error, y el error puede ser diferente cada vez. Esto obviamente causa confusión para los desarrolladores, incluyéndome a mí, ya que he tropezado aquí muchas veces.

Entonces, mi consejo es: por favor, desactiva esta función. Puedes desactivarla con las siguientes dos líneas de código:

no_shuffle();
run_tests;

En particular, no_shuffle se usa para desactivar la aleatorización y permite que las pruebas se ejecuten estrictamente en el orden de los casos de prueba.

reindex

El conjunto de casos de prueba de OpenResty tiene requisitos de formato estrictos. Cada caso de prueba necesita estar separado por tres saltos de línea, y la numeración de los casos de prueba tiene que ser estrictamente autoincremental.

Afortunadamente, tenemos una herramienta automática, reindex, para hacer este trabajo tedioso, que está oculta en el proyecto openresty-devel-utils. Como no hay documentación al respecto, solo unas pocas personas lo saben.

Si estás interesado, puedes intentar desordenar la numeración de los casos de prueba, o añadir o eliminar el número de saltos de línea, y luego usar esta herramienta para ordenarlo y ver si puedes restaurarlo.

Resumen

Esto es todo por la introducción a test::nginx. Por supuesto, hay más funciones, solo hemos hablado de las más importantes y centrales. "Dale un pescado a un hombre y lo alimentarás por un día; enséñale a pescar y lo alimentarás para toda la vida". Te he enseñado los métodos básicos y las precauciones para aprender pruebas, entonces puedes profundizar en el conjunto de casos de prueba oficial para una mejor comprensión.

Finalmente, piensa en las siguientes preguntas. ¿Hay pruebas en el desarrollo de tu proyecto? ¿Y qué marco usas para probar? Te invito a compartir este artículo con más personas para intercambiar y aprender juntos.