Experimenta con Elasticsearch: Sumérgete en nuestros cuadernos de muestra, inicia una prueba gratuita del cloud o prueba Elastic en tu máquina local ahora.
En esta entrada, presentaremos y explicaremos dos formas de probar software empleando Elasticsearch como dependencia externa del sistema. Cubriremos pruebas usando simulacros así como pruebas de integración, mostraremos algunas diferencias prácticas entre ellas y daremos algunas pistas sobre hacia dónde elegir cada estilo.
Buenas pruebas para la confianza del sistema
Una buena prueba es una prueba que aumenta la confianza de cada persona involucrada en el proceso de creación y mantenimiento de un sistema informático. Las pruebas no están pensadas para ser rápidas, geniales ni para aumentar artificialmente la cobertura del código. Las pruebas desempeñan un papel vital para garantizar que:
- Lo que queremos entregar va a funcionar en producción.
- El sistema cumple con los requisitos y los contratos.
- No habrá regresiones en el futuro.
- Los desarrolladores (y otros miembros del equipo implicados) están seguros de que lo que crearon funcionará.
Por supuesto, esto no significa que las pruebas no puedan ser frías, rápidas o aumentar la cobertura del código. Cuanto más rápido podamos ejecutar nuestra suite de pruebas, mejor. Simplemente, en la búsqueda de reducir la duración total de la suite de pruebas, no deberíamos sacrificar la fiabilidad, la mantenibilidad y la confianza que nos brindan las pruebas automatizadas.
Unas buenas pruebas automatizadas hacen que los distintos miembros del equipo se sientan más seguros:
- Desarrolladores: pueden confirmar que lo que hacen funciona (incluso antes de que el código en el que trabajan salga de su máquina).
- Equipo de control de calidad: tienen menos que probar manualmente.
- Los operadores de sistemas y los SRES: son más relajados, porque los sistemas son más fáciles de desplegar y mantener.
Por último, pero no menos importante: la arquitectura de un sistema. Nos encanta cuando los sistemas están organizados, son fáciles de mantener y la arquitectura es limpia y cumple su propósito. Sin embargo, a veces podemos ver una arquitectura que sacrifica demasiado por la excusa conocida como "así es más comprobable". No hay nada de malo en ser muy comprobable: solo cuando el sistema está escrito principalmente para ser comprobable en lugar de servir a las necesidades que justifican su existencia, vemos una situación en la que la cola mueve al perro.
Dos tipos de pruebas: Mocks y dependencias
Hay muchas formas en que las pruebas pueden ver y, por tanto, clasificar. En esta publicación me centraré solo en un aspecto de dividir los exámenes: usar mocks (o stubs, o fakes, o ...) frente a usar dependencias reales. En nuestro caso, la dependencia es Elasticsearch.
Los exámenes con mocks son muy rápidos porque no necesitan iniciar dependencias externas y todo ocurre solo en memoria. El mocking en pruebas automatizadas ocurre cuando se emplean objetos falsos en lugar de reales para probar partes de un programa sin usar las dependencias reales. Por eso son necesarias y por las que destacan en cualquier prueba de detección rápida en red, por ejemplo. Validación de la entrada. No es necesario abrir una base de datos y hacer una llamada solo para verificar que no se permiten números negativos en una solicitud, por ejemplo.
Sin embargo, introducir simulacros tiene varias participaciones:
- No todo y cada vez se puede burlar fácilmente, por eso los mocks tienen impacto en la arquitectura del sistema (que a veces es genial, a veces no tanto).
- Las pruebas que se ejecutan en mocks pueden ser rápidas, pero desarrollar tales pruebas puede llevar bastante tiempo porque las mocks que reflejan profundamente los sistemas que imitan normalmente no se ofrecen gratis. Alguien que sepa cómo funciona el sistema necesita escribir los mocks de la manera correcta, y este conocimiento puede venir de la experiencia práctica, el estudio de documentación, etc.
- Los simulacros deben mantener. Cuando tu sistema depende de una dependencia externa y necesitas actualizar esa dependencia, alguien tiene que cerciorar de que los mocks que imitan la dependencia también se actualicen con todos los cambios: fallos, documentados y no documentados (lo que también puede afectar a nuestro sistema). Esto resulta especialmente doloroso cuando quieres actualizar una dependencia pero tu suite de pruebas (usando solo simulaciones) no puede darte confianza en que todos los casos probados estén garantizados para funcionar.
- Se necesita disciplina para cerciorar que el esfuerzo se destine a desarrollar y probar el sistema, no a los simulacros.
Por estas razones, mucha gente defiende ir exactamente en la dirección opuesta: nunca usar mocks (o stubs, etc.), sino confiar únicamente en dependencias reales. Este enfoque funciona muy bien en demos o cuando el sistema es pequeño y solo tiene unos pocos casos de prueba que generan una gran cobertura. Estas pruebas pueden ser pruebas de integración (hablando a grandes rasgos: comprobar una parte de un sistema con dependencias reales) o pruebas de extremo a extremo (usando todas las dependencias reales al mismo tiempo y comprobando el comportamiento del sistema en todos los extremos, mientras se reproducen flujos de trabajo de usuario que definen el sistema como utilizable y exitoso). Un beneficio claro de este enfoque es que también verificamos (a menudo sin querer) nuestras suposiciones sobre las dependencias y cómo las integramos con el sistema en el que trabajamos.
Sin embargo, cuando las pruebas emplean únicamente dependencias reales, debemos considerar los siguientes aspectos:
- Algunos escenarios de prueba no necesitan la dependencia real (por ejemplo, para verificar los invariantes estáticos de una petición).
- Estas pruebas normalmente no se ejecutan en suites completas en las máquinas de los desarrolladores, porque esperar retroalimentación llevaría demasiado tiempo.
- Requieren más recursos en máquinas de CI, y puede que lleve más tiempo ajustar las cosas para no perder tiempo y recursos.
- Puede que no sea trivial inicializar dependencias con datos de prueba.
- Las pruebas con dependencias reales son ideales para cordonar el código antes de una refactorización importante, migración o actualización de dependencias.
- Es más probable que sean pruebas opacas, es decir, que no sean detalladas sobre los componentes internos del sistema en prueba, sino que cuiden sus resultados.
El punto óptimo: usa ambas pruebas
En lugar de probar tu sistema con un solo tipo de prueba, puedes confiar en ambos tipos donde tenga sentido e intentar mejorar tu uso de ambos.
- Ejecuta primero pruebas basadas en mocks porque son mucho más rápidas, y solo cuando todas tengan éxito, haz pruebas de dependencia más lentas solo después.
- Elige mocks para escenarios donde realmente no se necesitan dependencias externas: cuando el mocking llevaría demasiado tiempo, el código debería modificar mucho solo para eso; depender de dependencias externas.
- No hay nada de malo en probar un fragmento de código usando ambos enfoques, siempre que tenga sentido.
Ejemplo de SystemUnderTest
Para las siguientes secciones vamos a usar un ejemplo que se puede encontrar aquí. Es una pequeña aplicación demo escrita en Java 21, que emplea Maven como herramienta de compilación, que se basa en el cliente Elasticsearch y emplea la última incorporación de Elasticsearch, usando ES|QL (el nuevo lenguaje de consultas procedimentales de Elastic). Si Java no es tu lenguaje de programación, deberías poder entender los conceptos que vamos a discutir a continuación y traducirlos a tu stack. Simplemente usar un ejemplo real de código hace que ciertas cosas sean más fáciles de explicar.
El BookSearcher nos ayuda a gestionar la búsqueda y el análisis de datos, siendo en nuestro caso los libros (como se demostró en una de las entradas anteriores).
- Requiere Elasticsearch exactamente en la versión
8.15.xcomo su única dependencia (verisCompatibleWithBackend()), por ejemplo, porque no estamos seguros de si nuestro código es compatible hacia adelante, y estamos seguros de que no es compatible hacia atrás. Antes de actualizar Elasticsearch en producción a una versión más reciente, primero lo incluiremos en las pruebas para cerciorar que el comportamiento del Sistema Bajo Prueba se mantenga igual. - Podemos usarlo para buscar el número de libros publicados en un año determinado (ver
numberOfBooksPublishedInYear). - También podríamos emplearlo cuando necesitemos analizar nuestro conjunto de datos y encontrar los 20 autores más publicados entre dos años determinados (ver
mostPublishedAuthorsInYears).
Prueba con simulacros para empezar
Para crear los mocks usados en nuestros exámenes vamos a usar Mockito, una biblioteca de mocking muy popular en el ecosistema Java.
Podríamos empezar con lo siguiente, para que los simulacros se resetear antes de cada examen:
Como dijimos antes, no todo se puede evaluar fácilmente usando simulacros. Pero hay cosas que sí podemos (y probablemente deberíamos). Intentemos verificar que la única versión soportada de Elasticsearch es 8.15.x por ahora (en el futuro podríamos ampliar el rango una vez confirmemos que nuestro sistema es compatible con futuras versiones):
Podemos verificar de forma similar (simplemente devolviendo una versión menor diferente) que nuestro BookSearcher aún no va a funcionar con 8.16.x , porque no estamos seguros de si será compatible con él:
Ahora veamos cómo podemos lograr algo similar al probar contra un Elasticsearch real. Para esto vamos a usar el módulo Elasticsearch de Testcontainers, que solo tiene un requisito: necesita acceso a Docker, porque ejecuta contenedores Docker por ti. Desde cierto ángulo, los Testcontainers son simplemente una forma de operar contenedores Docker, pero en lugar de hacerlo en tu Docker Desktop (o similar), en tu LI o scripts, puedes expresar tus necesidades en el lenguaje de programación que conoces. Esto permite obtener imágenes, iniciar contenedores, recogerlas tras pruebas, copiar archivos de un lado a otro, ejecutar comandos, examinar registros, etc., directamente desde tu código de prueba.
El artículo puede ver así:
En este ejemplo dependemos de la integración de JUnit de Testcontainers con @Testcontainers y @Container, lo que significa que no tenemos que preocuparnos por iniciar Elasticsearch antes de nuestras pruebas y pararlo después. Lo único que tenemos que hacer es crear el cliente antes de cada prueba y cerrarlo luego de cada prueba (para evitar fugas de recursos, que podrían afectar a conjuntos de pruebas más grandes).
Anotar un campo no estático con @Container significa que se iniciará un nuevo contenedor para cada prueba, por lo que no tenemos que preocuparnos por datos obsoletos ni por resetear el estado del contenedor. Sin embargo, con muchas pruebas, este enfoque puede no funcionar bien, así que lo compararemos con alternativas en una de las próximas publicaciones.
Nota:
Al depender dedocker.elastic.co(el repositorio oficial de imágenes Docker de Elastic), evitas agotar tus límites en el hub Docker.
También se recomienda usar la misma versión de tu dependencia en tus entornos de pruebas y producción, para garantizar la máxima compatibilidad. También recomendamos ser precisos al seleccionar la versión, por lo que no hay etiquetalatestpara las imágenes de Elasticsearch.
Conexión con Elasticsearch en pruebas
El cliente Java de Elasticsearch es capaz de conectarse a Elasticsearch ejecutar en un contenedor de prueba incluso con seguridad y SSL/TLS activados (que son los valores predeterminados de las versiones 8.x, por eso no tuvimos que especificar nada relacionado con la seguridad en la declaración del contenedor). Suponiendo que el Elasticsearch que usas en producción también tenga TLS y algo de seguridad activados, se recomienda optar por la configuración de pruebas de integración lo más parecida posible al escenario de producción, y por tanto no desactivarlos en las pruebas.
Cómo obtener los datos necesarios para la conexión, suponiendo que el contenedor esté asignado a campo o variable elasticsearch:
elasticsearch.getHost()Te dará el host en el que se ejecuta el contenedor (que la mayoría de las veces probablemente será"localhost", pero por favor no lo codifiques de forma fija porque a veces, dependiendo de tu configuración, puede ser otro nombre, por lo que el host siempre debe obtener dinámicamente).elasticsearch.getMappedPort(9200)dará el puerto host que tienes que usar para conectarte a Elasticsearch que está dentro del contenedor (porque cada vez que inicias el contenedor, el puerto exterior cambia, así que también tiene que ser una llamada dinámica).- A menos que fueron sobreescribir por defecto, el nombre de usuario y la contraseña por defecto son
"elastic"y"changeme"respectivamente. - Si no se especificó ningún certificado SSL/TLS durante la configuración del contenedor, y la conectividad segura no está desactivada (que es el comportamiento por defecto de las versiones 8.x), se genera un certificado autofirmado. Confiar en ella (por ejemplo, como puede hacer cURL) el certificado puede obtener usando
elasticsearch.caCertAsBytes()(que devuelveOptional<byte[]>), u otra forma conveniente es obtenerSSLContextusandocreateSslContextFromCa().
El resultado general podría ser el siguiente:
Otro ejemplo de creación de una instancia de ElasticsearchClient se puede encontrar en el proyecto demo.
Nota:
Para crear clientes en entornos de producción, consulte la documentación.
Primera prueba de integración
Nuestra primera prueba, verificando que podemos crear BookSearcher usando Elasticsearch versión 8.15.x, podría ser así:
Como puedes ver, no necesitamos montar nada más. No necesitamos simular la versión devuelta por Elasticsearch, lo único que necesitamos es proporcionar BookSearcher le un cliente conectado a una instancia real de Elasticsearch, que fue iniciada para nosotros por Testcontainers.
A las pruebas de integración les importan menos los componentes internos
Hagamos un pequeño experimento: supongamos que tenemos que dejar de extraer datos del conjunto de resultados usando índices de columna, pero tenemos que confiar en los nombres de las columnas. Así que en el método isCompatibleWithBackend en lugar de
Vamos a tener:
Cuando volvamos a ejecutar ambas pruebas, notaremos que la prueba de integración con Elasticsearch real sigue pasando sin problemas. Sin embargo, los tests con mocks dejaron de funcionar, porque simulábamos llamadas como rs.getInt(int), no rs.getInt(String). Para que pasen, ahora tenemos que simularlos o simularlos a ambos, dependiendo de otros casos de uso que tengamos en nuestro conjunto de pruebas.
Las pruebas de integración pueden ser un cañón para matar a una mosca
Las pruebas de integración son capaces de verificar el comportamiento del sistema, incluso si no se necesitan dependencias externas. Sin embargo, usarlos de esta manera suele ser una pérdida de tiempo y recursos de ejecución. Veamos el método mostPublishedAuthorsInYears(int minYear, int maxYear). Las dos primeras líneas son las siguientes:
La primera afirmación es comprobar una condición, que no depende de Elasticsearch (ni de ninguna otra dependencia externa) de ninguna manera. Por lo tanto, no necesitamos iniciar ningún contenedor solo para verificar que, si el minYear es mayor que maxYear, se lanza una excepción.
Un simple examen de simulación, que además es rápido y no requiere muchos recursos, es más que suficiente para cerciorarlo. Luego de preparar los simulacros, simplemente podemos hacer lo siguiente:
Iniciar una dependencia, en lugar de burlar, sería un desperdicio en este caso de prueba porque no hay posibilidad de tomar una decisión significativa para esta dependencia.
Sin embargo, para verificar el comportamiento que empieza por String query = ..., que la consulta está correctamente escrita, se obtiene los resultados esperados: la biblioteca cliente es capaz de enviar solicitudes y respuestas adecuadas, no hay cambios de sintaxis y por tanto es mucho más fácil usar una prueba de integración, por ejemplo:
De este modo, podemos estar seguros de que cuando alimentemos nuestros datos a Elasticsearch (en esta o cualquier versión futura a la que elijamos migrar), nuestra consulta nos dará exactamente lo que esperábamos: el formato de datos no cambió, la consulta sigue siendo válida y todo el middleware (clientes, controladores, seguridad, etc.) seguirá funcionando. No tenemos que preocuparnos por mantener los mocks actualizados, el único cambio necesario para cerciorar la compatibilidad con, por ejemplo, 8.15 cambiaría esto:
Lo mismo ocurre si decides, por ejemplo, usar el buen y viejo QueryDSL en lugar de ES|QL: los resultados que recibes de la consulta (independientemente del lenguaje) deberían seguir siendo los mismos.
Emplea ambos enfoques cuando sea necesario
El caso del método mostPublishedAuthorsInYears ilustra que un solo método puede probar usando ambos métodos. Y quizá incluso debería estarlo.
- Usar solo mocks significa que tenemos que mantener el mock y no tener ninguna confianza al actualizar nuestro sistema.
- Usar solo pruebas de integración significaría que estamos desperdiciando bastantes recursos, sin necesidad de ellos en absoluto.
Vamos a recapitular
- Es posible usar tanto pruebas de simulación como de integración con Elasticsearch.
- Emplea las pruebas de simulación como una red de detección rápida y solo si pasan con éxito, inicia pruebas con dependencias (por ejemplo, usando
./mvnw test '-Dtest=!TestInt*' && ./mvnw test '-Dtest=TestInt*'o plugins Failsafe y Surefire ). - Usa mocks para probar el comportamiento de tu sistema ("líneas de código") donde la integración con dependencias externas realmente no importa (o incluso podría saltar).
- Emplea pruebas de integración para verificar tus suposiciones e integración con sistemas externos.
- No tengas miedo de probar usando ambos enfoques —si tiene sentido— según los puntos anteriores.
Se podría observar que ser tan estricto con la versión (en nuestro caso 8.15.x) es demasiado. Usar solo la etiqueta de versión podría serlo, pero ten en cuenta que en esta publicación sirve como representación de todas las demás características que puedan cambiar entre las versiones.
En la próxima entrega del serial, veremos formas de inicializar Elasticsearch ejecutar en un contenedor de prueba, con conjuntos de datos de prueba. Cuéntanos si creaste algo basado en este blog o si tienes preguntas en nuestros foros de discusión y en el canal comunitario de Slack.

