top of page
  • Foto del escritorRan Isenberg

Guía para pruebas sin servidor y Lambda - Parte 2 - Testing Pyramid


pirámide de pruebas sin servidor con funciones lambda
Pirámide de prueba

Las pruebas de software aumentan la calidad y la confiabilidad de las aplicaciones. Permiten a los desarrolladores encontrar y corregir errores de software, mitigar problemas de seguridad y simular casos de uso de usuarios reales.

Es una parte esencial del desarrollo de cualquier aplicación.


Serverless es una tecnología sorprendente, casi mágica. Las aplicaciones sin servidor, como cualquier otra aplicación, requieren pruebas.

Sin embargo, probar aplicaciones sin servidor difiere de las pruebas tradicionales e introduce nuevos desafíos.


En la primera parte , aprendió por qué los servicios sin servidor presentan nuevos desafíos de prueba y mis pautas prácticas para probar servicios sin servidor y funciones de AWS Lambda que mitigan estos desafíos.


En esta publicación, aprenderá a escribir pruebas para su servicio Serverless. Nos centraremos en las funciones Lambda y brindaremos consejos y trucos y ejemplos de código escribiendo pruebas para una aplicación Serverless real. Además, aprenderá mi adaptación Serverless a la pirámide de pruebas clásica y la implementará.


En la tercera parte , aprenderá a probar flujos controlados por eventos asincrónicos que pueden o no contener funciones Lambda y otros servicios sin servidor no basados en Lambda.


Puede encontrar aquí un proyecto de servicio sin servidor complementario que utiliza las mejores prácticas de pruebas sin servidor .

 

Tabla de contenido

 

El servicio de pedidos sin servidor

Utilizaremos este servicio de muestra y escribiremos pruebas para su controlador de función Lambda.

Aquí está el diagrama de arquitectura:


Servicio de muestras - el servicio de pedidos

El servicio 'pedido' recibe los pedidos de los clientes para un solo tipo de producto y guarda los pedidos en la base de datos.

Este servicio de muestra es simple: una API Gateway que activa una función Lambda que escribe en una tabla de DynamoDB. Además, la función Lambda utiliza AppConfig para la configuración dinámica y los indicadores de funciones.

Utilizo AWS CDK para implementar el servicio de pedidos.

Creé este servicio de plantilla sin servidor que incorporó muchas de las mejores prácticas en el dominio sin servidor, desde CI/CD y CDK hasta la escritura de buenos controladores de funciones.

Lea más sobre ello aquí y aquí .

 

La pirámide de pruebas sin servidor

Suposiciones

  1. Las definiciones que aparecen a continuación no son académicas, son mis definiciones. La definición no es tan importante como el contenido. Mientras pruebes estos aspectos de tu aplicación sin servidor, puedes confiar en la calidad general.

  2. Si bien no es obligatorio, es mejor que leas las pautas de pruebas sin servidor que presenté en la publicación anterior de la serie.

  3. Utilizo Python en los ejemplos, pero los principios son relevantes para la mayoría de los lenguajes de programación compatibles con Lambda.

Repasemos la pirámide de pruebas sin servidor y comprendamos los valores y objetivos que proporciona cada paso de la pirámide.


¿¡Una pirámide?!

La "pirámide de pruebas" es una metáfora que nos indica que debemos agrupar las pruebas de software en grupos de diferente granularidad. También nos da una idea de cuántas pruebas deberíamos tener en cada uno de estos grupos. Aunque el concepto de la pirámide de pruebas existe desde hace tiempo, los equipos aún tienen dificultades para ponerlo en práctica correctamente" - Martin Fowler

pirámide de pruebas sin servidor
Pirámide de pruebas sin servidor

Vamos a darle sentido a este diagrama.

El diagrama define cuatro niveles de pruebas: unitarias, de infraestructura, de integración y de extremo a extremo (E2E). Cada prueba tiene su propósito y sus características.

Siguiendo mis pautas de pruebas de Servleress, todas las pruebas se activan en el IDE y los desarrolladores pueden agregar puntos de interrupción a su código.

Cada tipo de prueba obtiene su carpeta en la estructura del proyecto bajo la carpeta principal '/tests'.

Como nota al margen, uso 'pytest' para aplicaciones basadas en Python para el motor de pruebas.


Repasemos los diferentes tipos de pruebas y sus características.

 

Pruebas unitarias

Las pruebas unitarias prueban la funcionalidad de unidades de código individuales. Estas pruebas están pensadas para ser rápidas , fáciles de depurar en el IDE con puntos de interrupción y no requieren implementación en AWS, lo que las hace aisladas .

Las pruebas unitarias entran en juego en el código que escribes, es decir, el código de la función Lambda.


Normalmente los utilizo en dos casos de uso:

  1. Lógica de validación de esquemas: utilizo Pydantic para casos de uso de validación de entrada y validación de esquemas (respuestas boto , respuestas de API, validación de entrada, etc.). El esquema de Pydantic puede contener verificaciones de restricciones de tipo y valor o incluso una lógica más complicada con el código de validación personalizado.

  2. Pruebe pequeñas funciones o módulos aislados que tengan entradas y salidas definidas. En este caso, quiero probar la lógica específica de una función o módulo interno y verificar su lógica y efectos secundarios. Si la función requiere llamadas a la API de AWS o recursos implementados, elimine la llamada o mueva la prueba a las pruebas de integración. No recomiendo llamar al controlador en este punto (se hará como parte de las pruebas de integración), sino solo a pequeñas funciones aisladas.

Si desea obtener más información sobre las mejores prácticas de validación de entrada para funciones de AWS Lambda, lea más aquí .


Ejemplos de pruebas unitarias

Vamos a escribir una prueba unitaria.

La función Lambda de creación de pedidos del servicio de pedidos espera un documento JSON que contenga dos parámetros: 'customer_name' y 'order_item_count'.

Nuestro esquema define 'customer_name' como una cadena con una longitud de entre 1 y 20 caracteres y 'order_item_count' como un entero positivo.


Un ejemplo de entrada válida se ve así:

Los esquemas Pydantic correspondientes se ven así:

Ahora, escribamos pruebas unitarias que verifiquen los tipos de entrada válidos e inválidos:

Las pruebas verifican tantos tipos de esquemas de error como sea posible, ya sea un tipo incorrecto o restricciones de valor.


La prueba en la línea 5 verifica la restricción de que el nombre del cliente sea una cadena no vacía, ya que espera que el esquema genere una excepción.

La prueba en la línea 21 verifica que un valor de recuento de elementos de pedido no entero genere una excepción.


Estas pruebas pueden parecer triviales, pero cuanto más lógica y parámetros agregue a sus esquemas, más aumentará la posibilidad de un error de producción.

" La validación de entrada debe aplicarse tanto a nivel sintáctico como semántico . - OWASP

Las pruebas unitarias del servicio de pedidos completo se pueden encontrar aquí .

 

Pruebas de infraestructura

Como se indica en las pautas de mi publicación anterior sobre pruebas de Servleress, el código de la aplicación y la infraestructura residen en el mismo proyecto y se implementan juntos.

Una vez que su código y su infraestructura estén implementados, no habrá vuelta atrás.

Entonces, queremos asegurarnos de que nuestra infraestructura esté configurada correctamente, que no falten recursos (los marcos IaC también tienen errores) y que no tengamos problemas de seguridad.

Queremos verificar estos aspectos antes de la implementación, para no interrumpir nuestro entorno de producción.


A medida que me familiarice más con las herramientas que utilizan CloudFormation, proporcionaré herramientas de prueba basadas en ellas. Las pruebas de infraestructura se realizarán sobre la plantilla de CloudFormation que estamos a punto de implementar y comprobarán numerosos problemas:

  1. Recursos críticos faltantes: se eliminó por error un depósito, una tabla de DynamoDB, etc.

  2. Cambio de ID lógico de recursos con estado: cuando cambia el ID lógico de un recurso, se elimina el recurso anterior y se vuelve a crear. En el caso de los recursos con estado, como DynamoDB, puede producirse una pérdida de datos.

  3. Problemas de seguridad: verificar que las definiciones de roles tengan el mínimo privilegio y que las configuraciones de recursos sean seguras: cifrado en REST, sin depósitos S3 públicos y más.


Para ver un ejemplo de pruebas de infraestructura específicas de AWS CDK, dirígete a mi blog de mejores prácticas de AWS CDK y consulta la sección "Pruebas de AWS CDK" y la sección "Los valores predeterminados de seguridad no son lo suficientemente buenos".

Para AWS SAM, consulte su linter .

Para plantillas genéricas de CloudFormation, consulte CFN-NAG .


Ejemplos de pruebas de infraestructura

Definamos una prueba de infraestructura de seguridad de CDK. La prueba cubre nuestra definición de servicio sin servidor, desde API Gateway hasta la función y el rol de Lambda y la tabla de DynamoDB.


La línea 14 sintetiza la plantilla de CloudFormation y la línea 11 ejecuta un conjunto de pruebas definidas por la matriz de soluciones de AWS . Se genera una excepción en caso de un problema de seguridad.

Puedes agregar más estándares de seguridad; ver más información aquí .


Ahora definamos una prueba de infraestructura CDK que verifique que nuestro recurso crítico, API Gateway, esté definido y no haya sido eliminado por error o falla.


La línea 13 sintetiza la plantilla de CloudFormation y la línea 16 afirma que hay un recurso de API Gateway. Puede ampliar esta prueba, verificar los identificadores lógicos de los recursos con estado y asegurarse de que no hayan cambiado.


Para conocer otras prácticas recomendadas de AWS CDK, consulte mi otra publicación .

Las pruebas completas de la infraestructura del servicio de pedidos se pueden encontrar aquí .

 

Pruebas de integración

Las pruebas de integración son el pan de cada día de las pruebas de función Lambda.

Ponen a prueba tu código y cómo se integra e interactúa con la infraestructura que creaste en AWS. Pones a prueba tu función Lambda, un módulo de software completo, ya sea un micro o nano servicio, desde el inicio hasta el final de su invocación.

Como tal, las pruebas de integración requieren la implementación de sus recursos en AWS y, por lo general:

  1. Ejecutar después de la fase de implementación en el pipeline CI/CD.

  2. Ejecutar localmente en IDE, permitir depuración con puntos de interrupción.

  3. Ejecútelo localmente en IDE bajo el rol y los permisos de desarrollador, no con el rol de Lambda.

  4. Llame al controlador de función con un evento de función Lambda generado para simular una invocación de integración Lambda real.

  5. Requiere configurar variables de entorno locales, ganchos o simulaciones requeridas por la función al comienzo de la prueba (ver conftest para Python).

  6. Puede llamar a las API y recursos de servicios de AWS.

  7. Suelen ser más lentos y menos aislados.

  8. Constituyen la mayoría de las pruebas de servicio.

  9. Contiene pruebas para casos extremos con simulacros (simulacros de fallas o excepciones generadas).

Mencioné en el punto 4 que debes generar el evento de función esperado. Se me ocurren al menos tres opciones para encontrar el ejemplo de esquema de evento:

  1. Genérelo en su cuenta de AWS, imprima el evento y cópielo y péguelo en una función de fábrica que lo devuelva para las pruebas de integración.

  2. Utilice este repositorio de esquemas. Contiene una cantidad absurda de esquemas de eventos de muestra.

  3. Lee la documentación del servicio que invoca tu función Lambda. Si bien no es perfecta, muchos servicios de AWS han mejorado la documentación y ahora incluyen eventos de muestra.

Por dónde empezar

Normalmente desarrollo un nuevo controlador de función Lambda escribiendo una prueba de integración de entrada de flujo "feliz" que llama a mi nueva función. El flujo feliz simula un caso de uso comercial real y una entrada. De esa manera, puedo depurar mi código localmente hasta que pase la prueba y usar recursos reales de AWS, también conocido como estilo TDD .


Otras pruebas deberían simular (con simulacros) los siguientes casos de uso:

  1. Errores de las API de AWS: verifiquemos que manejamos los errores correctamente, tal vez incluso reintentemos la acción y no se bloquee.

  2. Se generaron excepciones en capas internas, se verificó que se capturaran y que la respuesta de la función fuera correcta (código de error interno del servidor para HTTP, etc.).

  3. Entrada no válida: verifique que se devuelva un código de respuesta de solicitud incorrecta HTTP (cuando la función está detrás de una API Gateway).

  4. Configuración de indicadores de características: he escrito una publicación sobre cómo manejar pruebas con indicadores de características; léala aquí .

  5. Los efectos secundarios de la función Assert se produjeron correctamente: ¿su función guardó un elemento en la base de datos? ¿Contenía los parámetros esperados?


Ejemplos de pruebas de integración

Escribamos una prueba de integración para nuestra función Lambda de API "crear pedido". Lambda toma un evento de entrada, lo analiza y lo guarda en una tabla de DynamoDB.


Echemos un vistazo al controlador Lambda que deseamos probar.

El controlador Lambda recibirá la entrada, verificará la configuración, validará la entrada y llamará a la capa lógica para crear el pedido. A continuación, se muestra un fragmento de la firma del controlador:

Haga clic aquí para ver el código completo del controlador.


Ahora, comencemos a escribir la prueba de integración.

En pytest de Python, podemos usar archivos conftest para definir accesorios que se ejecutan antes de cualquier módulo de prueba y establecer simulacros globales o variables de entorno que nuestro controlador Lambda requiere.


Establecemos numerosas variables de entorno de controlador que necesitan el registrador, el rastreador y los indicadores de funciones. Además, la línea 18 define la variable para el nombre de la tabla DynamoDB en la que guardamos los pedidos. En el código CDK que define la tabla, establezco el nombre de la tabla como una salida de pila de CloudFormation para que se pueda cargar como una variable de entorno en la prueba sin esfuerzo. Es un buen truco y te recomiendo que lo hagas para todas las variables de entorno que necesites cargar en las pruebas de integración.

En la línea 21 creamos un recurso que inyectará el nombre de la tabla DynamoDB como argumento para nuestra prueba de controlador.


Ahora, echemos un vistazo a algunas de las pruebas de integración de este controlador.

Mira la primera prueba de flujo feliz: 'test_handler_200_ok' en la línea 10.

Cuando el controlador de orden de creación recibe un evento válido, esperamos que lo escriba en la tabla DynamoDB y devuelva el código HTTP 200 OK.


La línea 13 crea una muestra de carga útil de entrada válida de la API.

En la línea 14, activamos el controlador Lambda create_order con un evento generado que contiene la entrada válida y los demás atributos de metadatos de API Gateway. Ahora podemos agregar puntos de interrupción al controlador, depurar nuestra lógica y asegurarnos de que las pruebas se aprueben.


El método de fábrica de generación de eventos 'generate_api_gw_event' crea un evento de AWS API Gateway completo con la carga útil de prueba y se puede encontrar aquí .

Una vez finalizada con éxito, la prueba afirma en las líneas 16 a 20 que el esquema de respuesta es válido y contiene los valores esperados.

En las líneas 22 a 26, obtenemos el elemento insertado de la tabla DynamoDB y verificamos que la función escribió el elemento correctamente en la tabla. El nombre de la tabla se completó como argumento para la prueba (como vimos en el elemento 'table_name' de la prueba).


Servicios reales de AWS frente a simulaciones

Una ventaja importante que obtenemos al ejecutar las pruebas localmente con Pytest pero contra recursos reales de AWS es que podemos simular casi cualquier cosa. En la segunda prueba, 'test_internal_server_error', simulamos el recurso de tabla de AWS boto y simulamos un error del cliente de DynamoDB cuando no logramos guardar un elemento en la base de datos. Esta simulación nos permite probar nuestro código de reintento y la estrategia de cola de mensajes fallidos y verificar que el valor de retorno de la función, en este caso, es HTTP 500 .


En la línea 33, simulamos la función interna en la capa lógica de la función que crea un recurso de tabla 'boto'. La función simulada generará una excepción cuando se la llame.

La línea 37 afirma que la excepción se manejó correctamente y, en la línea 38, afirmamos que nuestro objeto de función simulada fue llamado para asegurarnos de que fue el que generó la excepción.


Podemos elegir simular cualquier lógica interna que queramos romper. Podemos usar recursos reales de AWS y simular solo algunos de ellos, según nuestra lógica. En última instancia, lo que desearíamos es cubrir todos los casos de uso en los que manejamos excepciones o errores y simulamos llamadas de API con fallas.


Las utilidades de cobertura de código pueden ayudarle a asegurarse de cubrir todas sus necesidades. Sin embargo, no garantizan que su controlador realmente funcione. Debe simular casos de uso comerciales reales.


Vea la prueba de integración completa aquí .

 

Pruebas de extremo a extremo (E2E)

Las pruebas de extremo a extremo tienen como objetivo ejecutarse contra los recursos implementados, simular casos de uso de clientes reales y activar procesos basados en eventos en toda su arquitectura.

Desea asegurarse de que su infraestructura esté configurada correctamente, que el evento recorra los recursos de AWS correctamente, que sus funciones de AWS Lambda se ejecuten con las variables de entorno correctas y que sus roles estén configurados con todos los permisos necesarios.


Generaremos localmente en el IDE el evento de inicio y verificaremos las respuestas.

A partir de ahí, todo el proceso se ejecuta en su cuenta de AWS y no tenemos ningún control sobre él. Por ello, estas son las pruebas más lentas de ejecutar, ya que prueban toda la cadena de principio a fin en la infraestructura.

Tenga en cuenta que no tenemos ninguna opción para simular fallas, por lo que recomiendo probar solo flujos satisfechos con el cliente y pruebas relacionadas con la seguridad (más sobre esto más adelante).

No sondearemos ni llamaremos a los recursos de AWS directamente, sino que utilizaremos llamadas API, de la misma manera que lo haría el cliente. Debemos enviar una llamada API REST a API Gateway y confirmar su respuesta. Cualquier efecto secundario ya se probó y demostró que funcionaba en las pruebas de integración que utilizan servicios REALES de AWS, por lo que no es necesario volver a probarlo, salvo para confirmar la respuesta de Lambda. Para ponerlo en contexto, en el servicio de pedidos, al crear un nuevo pedido, verifique que la respuesta sea válida y contenga los valores esperados. Sin embargo, no verifique la tabla DynamoDB directamente para el artículo insertado, sino que use las API REST orientadas al cliente, una API de "obtención de pedidos" (aún no existe en mi ejemplo, pero entiende la idea) para verificar que se insertó el artículo.


En la tercera parte de esta serie, analizaré cómo probar Step Functions y servicios asincrónicos, pero por ahora, centrémonos en el flujo sincrónico del servicio de pedidos.


Ejemplos de pruebas E2E

Echemos un vistazo a las pruebas de extremo a extremo a continuación:

En la línea 15, iniciamos el flujo feliz de un usuario que crea una solicitud de pedido válida.

Encontramos la URL completa del servicio con el mecanismo de salida de pila que hicimos en la prueba de integración para el nombre de la tabla.

La línea 17 genera la carga útil de entrada válida.

La línea 18 envía una solicitud de API POST REST a API Gateway.

La línea 19 afirma el código de respuesta de la función, y las líneas 21 a 23 afirman los datos de respuesta.


En la línea 26, probamos el manejo correcto de una entrada no válida.

Enviamos en la línea 28 un payload malformado (no coincide con el esquema) y esperamos en

líneas 29 a 31 para obtener un código de estado HTTP BAD REQUEST con un cuerpo JSON vacío.


Vea la prueba completa de extremo a extremo aquí .


Depuración de pruebas E2E

Las pruebas de integración pueden pasar, pero la variación E2E de la prueba puede fallar debido a permisos de roles mal configurados, importaciones faltantes en el paquete ZIP de la función Lambda, variables de entorno faltantes y otros casos de uso "divertidos".

La única forma de depurarlos es abrir los viejos registros de AWS CloudWatch, ver el error, implementar una versión corregida y volver a ejecutar la prueba.

¿Quieres aprender las mejores prácticas de registro de funciones Lambda? Consulta mi publicación aquí .


Pruebas de seguridad

Es fundamental probar el mecanismo de autenticación y autorización. Por lo general, estos mecanismos se implementan con un autorizador Lambda personalizado, una autorización IAM, un autorizador Cognito o un código personalizado en el controlador de funciones que realiza ambas cosas.

Estos mecanismos (todos excepto el código de función personalizado) se configuran en la parte IaC (CDK, SAM, etc.) y es fundamental garantizar que estén configurados correctamente y no se eliminen por accidente.

Por lo tanto, es importante invocar la función con permisos no válidos y asegurarse de que la función o API Gateway devuelva la respuesta HTTP 40X correcta. Sería mejor si simulara los siguientes casos de uso:

  1. Llame a su función con un token no válido (token expirado).

  2. Llame a su función con un token de autenticación válido (inicie sesión como usuario de prueba) pero con permisos no válidos (el usuario no puede ejecutar la API pero ha iniciado sesión en el sistema).

Tenga en cuenta que no incluí ningún mecanismo de autenticación/autorización en mi servicio de "pedido" de muestra, ya que habría complicado el ejemplo.


Lea más sobre esto aquí .


Pruebas de rendimiento

Monitorear el rendimiento de su servicio sin servidor y ajustarlo desde un aspecto de costo/rendimiento en relación con el tráfico de clientes esperado es esencial.

Sería mejor ejecutar estas pruebas ocasionalmente y al menos una vez antes de la puesta en producción de GA. Estas pruebas brindan información sobre los cuellos de botella y las conexiones ocultas del servicio y le permiten configurar mejores valores de concurrencia reservados o aprovisionados para su servicio.


Se recomienda utilizar herramientas como AWS X-Ray , AWS Lambda Power Tuning y la utilidad de seguimiento AWS Lambda Powertools. Lea más sobre esto aquí .

Puede encontrar más tareas de preparación para la producción sin servidor en mi publicación aquí .

 

La pirámide de pruebas y el flujo de trabajo de CI/CD

Mi canalización de CI/CD sin servidor recomendada ejecutará pruebas unitarias y de infraestructura, luego implementará la aplicación en AWS y ejecutará pruebas de integración y e2e.

El fallo en cualquiera de los pasos actúa como una puerta que hace fallar todo el proceso y le impide continuar al siguiente paso.


Para una canalización de CI/CD sin servidor basada en acciones de GitHub y CDK, lea mi publicación aquí .

 

Resumen: ¿Por qué funciona esto?

En la primera parte de la serie, enumeré los desafíos de las pruebas sin servidor. Al seguir las pautas que presenté allí e implementar la pirámide de pruebas sin servidor, pudimos mitigar la mayoría de los desafíos de las pruebas sin servidor, si no todos:

  1. Ofrecemos una buena experiencia para desarrolladores; podemos ejecutar la prueba desde el IDE y depurar localmente con integración y pruebas unitarias.

  2. Automatizamos todas nuestras pruebas.

  3. Ganamos confianza en que nuestro código funcionará en E2E porque utilizamos servicios reales de AWS incluso en las pruebas de integración.

  4. Probamos tanto nuestra configuración de infraestructura como el código de servicio en las pruebas.

  5. Cubrimos aspectos de infraestructura, rendimiento y costos en nuestras pruebas.

  6. Dirigimos toda la cadena de eventos desde el principio hasta el final.

  7. Simulamos fallas tanto en nuestra lógica como en las llamadas a la API de AWS.

  8. Cubrimos aspectos de validación de entrada.

En la tercera parte de la serie, aprenderá a probar AWS Step Functions, servicios Async Serverless y otros servicios Serverless no basados en Lambda.




Comments


bottom of page