Contenedores de pruebas - ¿Cómo facilitar las pruebas?
Bartlomiej Kuczynski
¿Buscas una manera de hacer tests de forma más sencilla? ¡Te hemos pillado! Consulta el siguiente artículo y aprende cómo hacerlo posible.
El desarrollo moderno de aplicaciones se basa en una regla muy sencilla:
Utilizar la composición
Componemos clases, funciones y servicios en piezas de software más grandes. Ese último elemento es la base de los microservicios y arquitectura hexagonal. Nos gustaría utilizar las soluciones existentes, integrarlas con nuestro software y pasar directamente a la mercado.
¿Quiere gestionar el registro de cuentas y almacenar los datos de los usuarios? Puede elegir uno de los servicios OAuth. ¿Tal vez tu aplicación ofrece algún tipo de suscripción o pago? Hay muchos servicios que pueden ayudarle a manejar esto. ¿Necesita algún tipo de análisis en su sitio web, pero no entiende GDPR? Siéntase libre y tome una de las soluciones listas para usar.
Algo que facilita tanto el desarrollo desde el punto de vista empresarial podría darle un dolor de cabeza: el momento en que necesita escribir una simple prueba.
Las bestias fantásticas: Colas, bases de datos y cómo probarlas
Las pruebas unitarias son bastante sencillas. Si sólo sigues las reglas, entonces tu entorno de pruebas y código son saludables. ¿Qué normas son ésas?
Fácil de escribir - una prueba unitaria debe ser fácil de escribir porque se escriben muchas. Menos esfuerzo significa que se escriben más pruebas.
Legible - el código de la prueba debe ser fácil de leer. La prueba es una historia. Describe el comportamiento del software y puede utilizarse como un atajo de documentación. Una buena prueba unitaria ayuda a corregir errores sin depurar el código.
Fiable - la prueba debe fallar sólo si hay un error en el sistema que se está probando. ¿Es obvio? No siempre. A veces las pruebas pasan si las ejecutas una a una, pero fallan cuando las ejecutas en conjunto. Pasan en tu máquina, pero fallan en CI (Funciona en mi máquina). Una buena prueba unitaria sólo tiene una razón para fallar.
Rápido - Las pruebas deben ser rápidas. La preparación para la ejecución, el inicio y la propia ejecución de las pruebas deben ser muy rápidos. De lo contrario, las escribirá, pero no las ejecutará. Las pruebas lentas significan pérdida de concentración. Esperas y miras la barra de progreso.
Independiente - por último, la prueba debe ser independiente. Esta regla se deriva de las anteriores. Sólo las pruebas verdaderamente independientes pueden constituir una unidad. No interfieren entre sí, pueden ejecutarse en cualquier orden y los posibles fallos no dependen de los resultados de otras pruebas. Independiente también significa que no depende de ningún recurso externo, como bases de datos, servicios de mensajería o sistemas de archivos. Si necesitas comunicarte con recursos externos, puedes utilizar mocks, stubs o dummies.
Todo se complica cuando queremos escribir algunas pruebas de integración. No está mal si queremos probar unos cuantos servicios juntos. Pero cuando necesitamos probar servicios que utilizan recursos externos como bases de datos o servicios de mensajería, entonces nos estamos buscando problemas.
Para ejecutar la prueba, debe instalar...
Hace muchos años, cuando queríamos hacer algunas pruebas de integración y utilizar, por ejemplo, bases de datos, teníamos dos opciones:
Podemos instalar una base de datos localmente. Configura un esquema y conéctate desde nuestra prueba;
Podemos conectarnos a una instancia existente "en algún lugar del espacio".
Ambas tienen pros y contras. Pero ambas introducen niveles adicionales de complejidad. A veces se trataba de complejidad técnica derivada de las características de determinadas herramientas, por ejemplo, la instalación y gestión de la base de datos Oracle en el host local. A veces era un inconveniente en el proceso, por ejemplo, tienes que ponerte de acuerdo con la prueba equipo sobre el uso de JMS... cada vez que quiera ejecutar pruebas.
Contenedores al rescate
En los últimos 10 años, la idea de la contenedorización ha ganado reconocimiento en la industria. Por lo tanto, una decisión natural es elegir los contenedores como solución para nuestro problema de pruebas de integración. Se trata de una solución sencilla y limpia. Basta con ejecutar la compilación del proceso y ¡todo funciona! ¿No te lo puedes creer? Echa un vistazo a esta sencilla configuración de una construcción maven:
com.dkanejs.maven.plugins
docker-compose-maven-plugin
4.0.0
up
prueba-compilación
arriba
${proyecto.basedir}/docker-compose.yml
true
abajo
posterior a la prueba de integración
down
${project.basedir}/docker-compose.yml
true
Y el docker-compose.yml Además, el expediente tiene muy buena pinta.
El ejemplo anterior es muy simple. Sólo una base de datos postgres, pgAdmin y eso es todo. Cuando ejecutes
bash
$ mvn clean verify
entonces el plugin de maven inicia los contenedores y después de las pruebas los apaga. Los problemas empiezan cuando el proyecto crece y nuestro fichero de composición crece también. Cada vez tendrás que arrancar todos los contenedores, y estarán vivos durante toda la compilación. Puedes mejorar un poco la situación cambiando la configuración de ejecución del plugin, pero no es suficiente. En el peor de los casos, ¡tus contenedores agotarán los recursos del sistema antes de que comiencen las pruebas!
Y este no es el único problema. No puedes ejecutar una sola prueba de integración desde tu IDE. Antes de eso, necesitas arrancar los contenedores a mano. Además, la siguiente ejecución de maven derribará esos contenedores (echa un vistazo a abajo ejecución).
Así que esta solución es como un gran carguero. Si todo funciona bien, no pasa nada. Cualquier comportamiento inesperado o poco común nos lleva a algún tipo de desastre.
Contenedores de prueba: ejecute contenedores desde las pruebas
Pero, ¿y si pudiéramos ejecutar nuestros contenedores desde las pruebas? Esta idea parece buena, y ya se está poniendo en práctica. Contenedores de pruebaya que estamos hablando de este proyecto, he aquí una solución para nuestros problemas. No es ideal, pero nadie es perfecto.
Se trata de un Java que soporta pruebas JUnit y Spock, proporcionando formas ligeras y fáciles de ejecutar el contenedor Docker. ¡Echémosle un vistazo y escribamos algo de código!
Requisitos previos y configuración
Antes de empezar, tenemos que comprobar nuestra configuración. Contenedores de prueba necesidad:
Docker en la versión v17.09,
Java versión mínima 1.8,
Acceso a la red, especialmente a docker.hub.
Encontrará más información sobre los requisitos para sistemas operativos y CI específicos en en documentación.
Ahora es el momento de añadir algunas líneas a pom.xml.
Utilizo Contenedores de prueba versión 1.17.3pero siéntase libre de utilizar el más reciente.
Pruebas con el contenedor Postgres
El primer paso es preparar nuestra instancia de un contenedor. Puedes hacerlo directamente en la prueba, pero una clase independiente queda mejor.
public class Postgres13TC extends PostgreSQLContainer {
private static final Postgres13TC TC = new Postgres13TC();
private Postgres13TC() {
super("postgres:13.2");
}
public static Postgres13TC getInstance() {
return TC;
}
@Override
public void start() {
super.start();
System.setProperty("DB_URL", TC.getJdbcUrl());
System.setProperty("DB_USERNAME", TC.getUsername());
System.setProperty("DB_PASSWORD", TC.getPassword());
}
@Override
public void stop() {
// no hace nada. Esta es una instancia compartida. Deja que la JVM se encargue de esta operación.
}
}
Al principio de las pruebas, crearemos una instancia de Postgres13TC. Esta clase puede manejar información sobre nuestro contenedor. Lo más importante aquí son las cadenas de conexión a la base de datos y las credenciales. Ahora es el momento de escribir una prueba muy simple.
Aquí utilizo JUnit 5. Anotación @Testcontainers forma parte de las extensiones que controlan los contenedores en el entorno de prueba. Encuentran todos los campos con @Contenedor y los contenedores de inicio y parada, respectivamente.
Pruebas con Spring Boot
Como he mencionado antes, utilizo Spring Boot en el proyecto. En este caso, tenemos que escribir un poco más de código. El primer paso es crear una clase de configuración adicional.
Esta clase sustituye las propiedades existentes con valores de la clase contenedor de prueba. Las tres primeras propiedades son propiedades estándar de Spring. Las cinco siguientes son propiedades adicionales personalizadas que se pueden utilizar para configurar otros recursos y extensiones como liquibase, por ejemplo:
@SpringBootTest(webEnvironment = RANDOM_PORT) - marca la prueba como una prueba de Spring Boot e inicia el contexto de Spring.
@AutoConfigureTestDatabase(replace = NONE) - estas anotaciones dicen que la extensión de prueba de spring no debe reemplazar la configuración de la base de datos postgres con H2 en la configuración de memoria.
@ContextConfiguration(inicializadores = ContainerInit.class) - un contexto primaveral adicional donde establecemos las propiedades de Contenedores de prueba.
@Testcontainers - como se ha mencionado anteriormente, esta anotación controla el ciclo de vida del contenedor.
En este ejemplo, utilizo repositorios reactivos, pero funciona igual con repositorios JDBC y JPA comunes.
Ahora podemos ejecutar esta prueba. Si es la primera vez que se ejecuta, el motor necesita extraer imágenes de docker.hub. Esto puede tardar un momento. Después de eso, veremos que dos contenedores se han ejecutado. Uno es postgres y el otro es el controlador Testcontainers. Ese segundo contenedor gestiona los contenedores en ejecución e incluso si la JVM se detiene inesperadamente, entonces apaga los contenedores y limpia el entorno.
Resumamos
Contenedores de prueba son herramientas muy fáciles de usar que nos ayudan a crear pruebas de integración que utilizan contenedores Docker. Eso nos da más flexibilidad y aumenta la velocidad de desarrollo. La configuración adecuada de las pruebas reduce el tiempo necesario para embarcar a los nuevos desarrolladores. No necesitan configurar todas las dependencias, solo ejecutar las pruebas escritas con los archivos de configuración seleccionados.