Métodos de prueba de `test::nginx`: Configuración, envío de solicitudes y manejo de respuestas
API7.ai
November 18, 2022
En el último artículo, ya tuvimos un primer vistazo de test::nginx
y ejecutamos el ejemplo más simple. Sin embargo, en un proyecto de código abierto real, los casos de prueba escritos en test::nginx
son mucho más complejos y difíciles de dominar que el código de muestra. De lo contrario, no se llamaría un obstáculo.
En este artículo, te guiaré a través de los comandos y métodos de prueba más utilizados en test::nginx
, para que puedas entender la mayoría de los conjuntos de casos de prueba en el proyecto OpenResty y tengas la capacidad de escribir casos de prueba más realistas. Incluso si aún no has contribuido con código a OpenResty, familiarizarte con el marco de pruebas de OpenResty será una gran inspiración para que diseñes y escribas casos de prueba en tu trabajo.
La prueba de test::nginx
esencialmente genera nginx.conf
e inicia un proceso de NGINX basado en la configuración de cada caso de prueba. Luego, simula una solicitud de cliente con el cuerpo y los encabezados especificados. A continuación, el código Lua en el caso de prueba procesa la solicitud y realiza una respuesta. En este momento, test::nginx
analiza la información crítica como el cuerpo de la respuesta, los encabezados de la respuesta y los registros de errores, y los compara con la configuración de la prueba. Si existe una discrepancia, la prueba falla con un error; de lo contrario, es exitosa.
test::nginx
proporciona muchos primitivos DSL (Lenguaje específico del dominio). He hecho una clasificación simple según la configuración de NGINX, el envío de solicitudes, el procesamiento de respuestas y la verificación de registros. Este 20% de la funcionalidad puede cubrir el 80% de los casos de aplicación, por lo que debemos tener un buen dominio de ello. En cuanto a otros primitivos y usos más avanzados, los presentaremos en el próximo artículo.
Configuración de NGINX
Primero, veamos la configuración de NGINX. El primitivo de test::nginx
con la palabra clave "config" está relacionado con la configuración de NGINX, como config
, stream_config
, http_config
, etc.
Sus funciones son las mismas: insertar la configuración de NGINX especificada en diferentes contextos de NGINX. Estas configuraciones pueden ser comandos de NGINX o código Lua encapsulado en content_by_lua_block
.
Al realizar pruebas unitarias, config
es el primitivo más utilizado, en el que cargamos bibliotecas Lua y llamamos a funciones para pruebas de caja blanca. Aquí hay un fragmento de código de prueba, que no se puede ejecutar completamente. Proviene de un proyecto de código abierto real, por lo que si estás interesado, puedes hacer clic en el enlace para ver la prueba completa, o puedes intentar ejecutarla 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")
}
}
El propósito de este caso de prueba es probar si la función check_schema
en el archivo de código plugins.key-auth
funciona correctamente. Utiliza el comando NGINX content_by_lua_block
en location /t
para requerir el módulo a probar y llamar directamente a la función que necesita ser verificada.
Este es un medio común de pruebas de caja blanca en test::nginx
. Sin embargo, esta configuración por sí sola no es suficiente para completar la prueba, así que sigamos adelante y veamos cómo enviar una solicitud de cliente.
Envío de Solicitudes
Simular un cliente que envía una solicitud implica bastantes detalles, así que comencemos con el más simple: enviar una sola solicitud.
request
Continuando con el caso de prueba anterior, si queremos que el código de prueba unitaria se ejecute, entonces debemos iniciar una solicitud HTTP a la dirección /t
especificada en la configuración, como se muestra en el siguiente código de prueba:
--- request
GET /t
Este código envía una solicitud GET
a /t
en el primitivo de solicitud. Aquí, no especificamos la dirección IP, el nombre de dominio o el puerto de acceso, ni especificamos si es HTTP 1.0
o HTTP 1.1
. Todos estos detalles están ocultos por test::nginx
, por lo que no tenemos que preocuparnos. Este es uno de los beneficios de DSL: solo necesitamos enfocarnos en la lógica del negocio sin distraernos con todos los detalles.
Además, esto proporciona cierta flexibilidad. Por ejemplo, el protocolo predeterminado es HTTP 1.1
, o si queremos probar HTTP 1.0
, podemos especificarlo por separado:
--- request
GET /t HTTP/1.0
Además del método GET
, también se necesita soportar el método POST
. En el siguiente ejemplo, podemos enviar la cadena hello world
a la dirección especificada.
--- request
POST /t
hello world
Nuevamente, test::nginx
calcula la longitud del cuerpo de la solicitud por ti aquí, y agrega los encabezados de solicitud host
y connection
para asegurarse de que esta sea una solicitud normal automáticamente.
Por supuesto, podemos agregar comentarios para que sea más legible. Los que comienzan con #
serán reconocidos como comentarios de código.
--- request
# post request
POST /t
hello world
La solicitud también admite un modo más complejo y flexible, que utiliza eval
como filtro para incrustar código Perl directamente, ya que test::nginx
está escrito en Perl. Si el lenguaje DSL actual no satisface tus necesidades, eval
es la "última arma" para ejecutar código Perl directamente.
Para el uso de eval
, veamos algunos ejemplos simples aquí, y continuaremos con otros más complejos en el próximo artículo.
--- request eval
"POST /t
hello\x00\x01\x02
world\x03\x04\xff"
En el primer ejemplo, usamos eval
para especificar caracteres no imprimibles, que es uno de sus usos. El contenido entre las comillas dobles se tratará como una cadena Perl y luego se pasará a request
como argumento.
Aquí hay un ejemplo más interesante:
--- request eval
"POST /t\n" . "a" x 1024
Sin embargo, para entender este ejemplo, necesitamos saber algo sobre las cadenas en Perl, así que mencionaré brevemente dos puntos aquí.
- En Perl, usamos un punto para representar la concatenación de cadenas. ¿No es esto algo similar a los dos puntos de
Lua
? - Una
x
minúscula indica el número de veces que se repite un carácter. Por ejemplo, el"a" x 1024
anterior significa que el carácter "a" se repite 1024 veces.
Entonces, el segundo ejemplo significa que el método POST
envía una solicitud que contiene 1024
caracteres a
a la dirección /t
.
pipelined_requests
Después de entender cómo enviar una sola solicitud, veamos cómo enviar múltiples solicitudes. En test::nginx
, podemos usar el primitivo pipelined_requests
para enviar múltiples solicitudes en secuencia dentro de la misma conexión keep-alive
:
--- pipelined_requests eval
["GET /hello", "GET /world", "GET /foo", "GET /bar"]
Por ejemplo, este ejemplo accederá a estas cuatro API secuencialmente en la misma conexión. Hay dos ventajas sobre esto:
- La primera es que se puede eliminar mucho código de prueba repetitivo, y los cuatro casos de prueba se pueden comprimir en uno.
- La segunda y más importante razón es que podemos usar solicitudes en pipelining para detectar si la lógica del código tendrá excepciones en el caso de múltiples accesos.
Puedes preguntarte, si escribo múltiples casos de prueba en secuencia, entonces el código también se ejecutará varias veces en la fase de ejecución. ¿No cubre eso también el segundo problema anterior?
Se reduce al modo de ejecución de test::nginx
, que funciona de manera diferente a lo que podrías pensar. Después de cada caso de prueba, test::nginx
cierra el proceso actual de NGINX, y todos los datos en memoria desaparecen. Al ejecutar el siguiente caso de prueba, se regenera nginx.conf
y se inicia un nuevo Worker
de NGINX. Este mecanismo asegura que los casos de prueba no se afecten entre sí.
Por lo tanto, cuando queremos probar múltiples solicitudes, necesitamos usar el primitivo pipelined_requests
. Basándonos en él, podemos simular limitaciones de tasa, limitaciones de concurrencia y muchos otros escenarios para probar si tu sistema funciona correctamente con escenarios más realistas y complejos. Lo dejaremos para el próximo artículo también, ya que involucrará múltiples comandos y primitivos.
repeat_each
Acabamos de mencionar el caso de probar múltiples solicitudes, entonces, ¿cómo deberíamos ejecutar la misma prueba múltiples veces?
Para este problema, test::nginx
proporciona una configuración global: repeat_each
, que es una función Perl que por defecto es repeat_each(1)
, lo que indica que el caso de prueba solo se ejecutará una vez. Entonces, en los casos de prueba anteriores, no nos molestamos en configurarlo por separado.
Naturalmente, podemos configurarlo antes de la función run_test()
, por ejemplo, cambiando el argumento a 2
.
repeat_each(2);
run_tests();
Entonces, cada caso de prueba se ejecuta dos veces, y así sucesivamente.
more_headers
Después de hablar sobre el cuerpo de la solicitud, veamos los encabezados de la solicitud. Como mencionamos anteriormente, test::nginx
envía la solicitud con los encabezados host
y connection
por defecto. ¿Qué pasa con los otros encabezados de la solicitud?
more_headers
está diseñado específicamente para hacer eso.
--- more_headers
X-Foo: blah
Podemos usarlo para configurar varios encabezados personalizados. Si queremos configurar más de un encabezado, entonces configuramos más de una línea:
--- more_headers
X-Foo: 3
User-Agent: openresty
Manejo de Respuestas
Después de enviar la solicitud, la parte más importante de test::nginx
es procesar la respuesta, donde determinaremos si la respuesta cumple con las expectativas. Aquí lo dividimos en cuatro partes y las presentamos: el cuerpo de la respuesta, el encabezado de la respuesta, el código de estado de la respuesta y el registro.
response_body
El contraparte del primitivo de solicitud es response_body
, y el siguiente es un ejemplo de sus dos configuraciones en uso:
=== TEST 1: sanity
--- config
location /t {
content_by_lua_block {
ngx.say("hello")
}
}
--- request
GET /t
--- response_body
hello
Este caso de prueba pasará si el cuerpo de la respuesta es hello
, y reportará un error en otros casos. Pero, ¿cómo probamos un cuerpo de respuesta largo? No te preocupes, test::nginx
ya se ha encargado de eso por ti. Admite la detección del cuerpo de la respuesta con una expresión regular, como la siguiente:
--- response_body_like
^he\w+$
Esto te permite ser muy flexible con el cuerpo de la respuesta. Además, test::nginx
también admite operaciones unlike
:
--- response_body_unlike
^he\w+$
En este punto, si el cuerpo de la respuesta es hello
, la prueba no pasará.
En la misma línea, después de entender la detección de una sola solicitud, veamos la detección de múltiples solicitudes. Aquí hay un ejemplo de cómo usarlo con pipelined_requests
:
--- pipelined_requests eval
["GET /hello", "GET /world", "GET /foo", "GET /bar"]
--- response_body eval
["hello", "world", "oo", "bar"]
Por supuesto, lo importante a tener en cuenta aquí es que tantas solicitudes como envíes, necesitas tener tantas respuestas para corresponder.
response_headers
En segundo lugar, hablemos del encabezado de la respuesta. El encabezado de la respuesta es similar al encabezado de la solicitud en que cada línea corresponde a la clave y el valor de un encabezado.
--- response_headers
X-RateLimit-Limit: 2
X-RateLimit-Remaining: 1
Al igual que la detección del cuerpo de la respuesta, los encabezados de la respuesta también admiten expresiones regulares y operaciones unlike
, como response_headers_like
, raw_response_headers_like
y raw_response_headers_unlike
.
error_code
El tercero es el código de respuesta. La detección del código de respuesta admite comparación directa y también admite operaciones like
, como los siguientes dos ejemplos:
--- error_code: 302
--- error_code_like: ^(?:500)?$
En el caso de múltiples solicitudes, el error_code
necesita ser verificado múltiples veces:
--- pipelined_requests eval
["GET /hello", "GET /hello", "GET /hello", "GET /hello"]
--- error_code eval
[200, 200, 503, 503]
error_log
El último elemento de prueba es el registro de errores. En la mayoría de los casos de prueba, no se genera ningún registro de errores. Podemos usar no_error_log
para detectar:
--- no_error_log
[error]
En el ejemplo anterior, si la cadena [error]
aparece en el error.log
de NGINX, la prueba fallará. Esta es una característica muy común, y se recomienda que agregues la detección del registro de errores a todas tus pruebas normales.
--- error_log
hello world
La configuración anterior está detectando la presencia de hello world
en error.log
. Por supuesto, puedes usar eval
incrustado en código Perl para implementar la detección de expresiones regulares, como la siguiente:
--- error_log eval
qr/\[notice\] .*? \d+ hello world/
Resumen
Hoy, estamos aprendiendo cómo enviar solicitudes y probar respuestas en test::nginx
, incluyendo el cuerpo de la solicitud, el encabezado, el código de estado de la respuesta y el registro de errores. Podemos implementar un conjunto completo de casos de prueba con la combinación de estos primitivos.
Finalmente, aquí hay una pregunta para reflexionar: ¿Cuáles son las ventajas y desventajas de test::nginx
, un DSL abstracto? Siéntete libre de dejar comentarios y discutir conmigo, y también eres bienvenido a compartir este artículo para comunicarte y pensar juntos.