<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Piotr Przybyl - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Piotr Przybyl - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/es/search-labs/author/piotr-przybyl</link>
    </image>
    <link>https://www.elastic.co/es/search-labs/author/piotr-przybyl</link>
    <atom:link href="https://www.elastic.co/es/search-labs/rss/author/piotr-przybyl.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[es]]></language>
    <lastBuildDate>Mon, 05 Oct 2026 15:47:16 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Fragmentos y réplicas de Elasticsearch: Una guía práctica]]></title>
    <description><![CDATA[Domina los conceptos de fragmentos y réplicas de Elasticsearch y aprende a optimizarlos.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch potencia Lucene construyendo un sistema distribuido sobre él, que aborda los problemas de escalabilidad y tolerancia a fallos. También expone una API REST basada en JSON, lo que facilita mucho la interoperabilidad con otros sistemas.</p><p>Sistemas distribuidos como Elasticsearch pueden ser muy complejos, con muchos factores que pueden afectar su rendimiento y estabilidad. <strong>Los fragmentos</strong> son uno de los conceptos más fundamentales en Elasticsearch, y entender cómo funcionan te permitirá gestionar eficazmente un clúster de Elasticsearch.</p><p>Este artículo explica qué son los shards primarios y réplica, su impacto en un clúster de Elasticsearch y qué herramientas existen para ajustarlos a diferentes demandas.</p><h2>Entendiendo fragmentos</h2><p>Los datos en un índice de Elasticsearch pueden crecer a proporciones enormes. Para mantenerlo manejable, cada dato se almacena en un índice, y los índices son un índice <strong>dividido en varios fragmentos</strong>. Cada fragmento de Elasticsearch es un índice de Lucene Apache, donde cada índice individual de Lucene contiene un subconjunto de los documentos del índice de Elasticsearch. Dividir los índices de esta manera mantiene el uso de recursos bajo control. Un índice de Lucena apache tiene un límite de 2.147.483.519 (2³¹ - 129) documentos.</p><p>A veces, es necesario mover índices entre nodos para fines de reequilibrio. Dado que este proceso puede requerir tanto tiempo como recursos, los índices no deberían crecer demasiado, lo que ayuda a mantener el tiempo de recuperación manejable. Además, dado que los índices están compuestos por segmentos de Lucene que deben fusionar constantemente, es importante que los segmentos no se hagan demasiado grandes. Por estas razones, Elasticsearch divide los datos del índice en fragmentos más pequeños y manejables, <strong>llamados fragmentos primarios</strong>, que pueden distribuir más fácilmente entre varias máquinas. <strong>Los fragmentos réplica</strong> son simplemente una copia exacta de un fragmento primario correspondiente y repasaremos su función más adelante en este artículo.</p><p>Tener el número adecuado de fragmentos es importante para el rendimiento. Por tanto, es prudente planear con antelación. Cuando las consultas se ejecutan en diferentes fragmentos en paralelo, se ejecutan más rápido que un índice compuesto por un solo fragmento, pero solo si cada fragmento está ubicado en un nodo diferente y hay suficientes nodos en el clúster. Sin embargo, al mismo tiempo, los fragmentos consumen memoria y espacio en disco, tanto en términos de datos indexados como de metadatos de clúster. Tener demasiados fragmentos (también conocido como sobrefragmentación) puede ralentizar consultas, solicitudes de indexación y operaciones de gestión, por lo que mantener el equilibrio adecuado es fundamental.</p><p>El número de fragmentos primarios se define en el momento de la creación <strong>del índice para esa instancia específica del índice</strong>. Si necesitas un número diferente de fragmentos primarios más adelante, puedes usar las<strong> APIs de redimensionamiento</strong>: <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-split">split</a> (más shards primarios), <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-shrink">shrink</a> (menos shards primarios) o <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-clone">clone</a> (el mismo número de shards primarios con nuevos ajustes para réplicas). Estas operaciones copian los segmentos de Lucene y <strong>evitan una reindexación completa de todos los documentos</strong>. Al crear un índice, puedes establecer el número de fragmentos primarios y réplica como ajustes del índice:</p>PUT /sensor
{
   "settings" : {
       "index" : {
           "number_of_shards" : 6,
           "number_of_replicas" : 2
       }
   }
}<p>(Si no especificas el número de fragmentos o réplicas, el valor por defecto de ambos es 1, según Elasticsearch 7.0). El número ideal de fragmentos debe determinar en función de la cantidad de datos en un índice. Generalmente, <a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/size-shards">un shard óptimo debe contener entre 10 y 50GB de datos</a>, con menos de 200 millones de documentos por shard. Por ejemplo, si esperas acumular alrededor de 300GB de registros de aplicaciones en un día, tener alrededor de 10 fragmentos en ese índice sería razonable, siempre que tengas suficientes nodos para alojarlos.</p><p>Durante su vida, los fragmentos pueden pasar por varios estados, entre ellos:</p><ul><li><p><strong>Inicializar:</strong> Un estado inicial antes de que se pueda usar el fragmento.</p></li><li><p><strong>Comenzó:</strong> Un estado en el que el fragmento está activo y puede recibir solicitudes.</p></li><li><p><strong>Reubicación:</strong> Un estado que ocurre cuando los fragmentos están en proceso de mover a otro nodo. Esto puede ser necesario bajo ciertas condiciones, por ejemplo, cuando el nodo en el que están se está quedando sin espacio en disco.</p></li><li><p><strong>No asignado:</strong> El estado de un fragmento que no fue asignado. Se proporciona una razón cuando esto ocurre, por ejemplo, si el nodo que aloja el fragmento ya no está en el clúster <em>(NODE_LEFT)</em> o debido a la restauración en un índice cerrado <em>(EXISTING_INDEX_RESTORED).</em></p></li></ul><p>Para ver todos los fragmentos, sus estados y otros metadatos, puedes usar la siguiente solicitud:</p>GET _cat/shards<p>Para ver fragmentos de un índice específico, puedes agregar el nombre del índice a la URL, por ejemplo, sensor:</p>GET _cat/shards/sensor<p>Este comando produce una salida, como en el siguiente ejemplo. Por defecto, las columnas que aparecen incluyen el nombre del índice, el nombre (es decir, número) del fragmento, si es un fragmento principal o una réplica, su estado, el número de documentos, el tamaño en disco, así como la dirección IP y el ID del nodo donde se encuentra el fragmento.</p>sensor 5 p STARTED    0  283b 127.0.0.1 ziap
sensor 5 r UNASSIGNED                  
sensor 2 p STARTED    1 3.7kb 127.0.0.1 ziap
sensor 2 r UNASSIGNED                  
sensor 3 p STARTED    3 7.2kb 127.0.0.1 ziap
sensor 3 r UNASSIGNED                  
sensor 1 p STARTED    1 3.7kb 127.0.0.1 ziap
sensor 1 r UNASSIGNED                  
sensor 4 p STARTED    2 3.8kb 127.0.0.1 ziap
sensor 4 r UNASSIGNED                  
sensor 0 p STARTED    0  283b 127.0.0.1 ziap
sensor 0 r UNASSIGNED<h2>Comprensión de réplicas</h2><p>Aunque cada fragmento contiene una única copia de los datos, un índice puede contener varias copias del fragmento. Por tanto, existen dos tipos de fragmentos: el <strong>fragmento principal</strong> y una copia, o <strong>réplica</strong>. Cada réplica de un fragmento primario siempre se encuentra en un nodo diferente, lo que garantiza una alta disponibilidad de tus datos en caso de fallo de un nodo. Además de la redundancia y su papel en la prevención de la pérdida de datos y el tiempo de inactividad, las réplicas también pueden ayudar a mejorar el rendimiento de búsqueda al permitir que las consultas se procesen en paralelo con el shard primario y, por tanto, más rápido.</p><p>Existen diferencias importantes en el comportamiento de los fragmentos primarios y réplica. Aunque ambos son capaces de procesar consultas, las solicitudes de indexación (es decir, Agregar datos al índice) debe pasar primero por los fragmentos primarios antes de poder replicar en los fragmentos réplica. Como se indicó antes, si un fragmento primario deja de estar disponible—por ejemplo, debido a una desconexión de nodo o fallo de hardware—se promueve una réplica para asumir su función.</p><p>Aunque las réplicas pueden ayudar en caso de fallo de un nodo, es importante no tener demasiadas porque consumen memoria, espacio en disco y potencia de cálculo al indexar. Otra diferencia entre los fragmentos primarios y las réplicas es que, aunque el número de fragmentos primarios no puede cambiar una vez creado el índice, el número de réplicas puede modificar dinámicamente en cualquier momento actualizando la configuración del índice.</p><p>Otro factor a considerar con las réplicas es el número de nodos disponibles. Las réplicas siempre se colocan en nodos diferentes del fragmento primario, ya que dos copias de los mismos datos en el mismo nodo no ofrecerían protección si el nodo fallara. Como resultado, para que un sistema soporte <em>n</em> réplicas, debe haber al <em>menos n + 1</em> nodos en el clúster. Por ejemplo, si hay dos nodos en un clúster y un índice está configurado con seis réplicas, solo se asignará una réplica. Por otro lado, un sistema con siete nodos es perfectamente capaz de manejar un fragmento principal y seis réplicas.</p><h2>Optimización de fragmentos y réplicas</h2><p>Incluso después de que se creó un índice con el equilibrio adecuado entre fragmentos primarios y réplica, estos deben ser monitorizados, ya que la dinámica alrededor de un índice cambia con el tiempo. Por ejemplo, al tratar con datos de seriales temporales, los índices con datos recientes suelen estar más activos que los más antiguos. Sin ajustar estos índices, todos consumirían la misma cantidad de recursos, a pesar de sus requisitos muy diferentes.</p><p>La API de índices de rollover puede usar para separar índices más nuevos y antiguos. Se puede configurar para crear automáticamente un nuevo índice una vez alcanzado cierto umbral—el tamaño del índice en el disco, el número de documentos o la antigüedad. Esta API también es útil para mantener bajo control el tamaño de los fragmentos. Dado que el número de fragmentos no puede modificar fácilmente tras la creación del índice, los fragmentos seguirán acumulando datos si no se cumplen las condiciones de rollover. Para índices antiguos que solo requieren acceso poco frecuente, reducir y forzar la fusión de un índice son dos formas diferentes de reducir su huella de memoria y disco. La primera reduce el número de fragmentos en un índice, mientras que la segunda reduce el número de segmentos Lucene y libera espacio empleado por documentos que fueron eliminados.</p><h2>Fragmentos primarios y réplica como base de Elasticsearch</h2><p>Elasticsearch construyó una estable reputación como plataforma distribuida de almacenamiento, búsqueda y análisis para enormes volúmenes de datos. Sin embargo, al operar a tal escala, inevitablemente surgen desafíos. Por eso entender cómo funcionan los fragmentos primarios y réplica es tan importante y fundamental para Elasticsearch, ya que esto puede ayudar a optimizar la fiabilidad y el rendimiento de la plataforma.</p><p>Saber cómo funcionan y cómo optimizarlos es fundamental para lograr un clúster de Elasticsearch más robusto y eficiente. Si experimentas respuestas lentas o cortes de información con regularidad, este conocimiento puede ser la clave para superar estos obstáculos.</p><p>Sigue la documentación oficial de Elasticsearch para saber más sobre <a href="https://www.elastic.co/docs/deploy-manage/distributed-architecture/clusters-nodes-shards">clústeres, nodos y fragmentos</a>, <a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/size-shards">cómo dimensionar tus fragmentos</a>, <a href="https://www.elastic.co/docs/deploy-manage/distributed-architecture/shard-allocation-relocation-recovery">asignación y recuperación de fragmentos</a>.</p><p>Este tema también está disponible como curso introductorio en el <a href="https://youtu.be/sAySPSyL2qE">canal de YouTube de Elastic Community.</a></p><p>Por último, pero no menos importante: si no quieres preocuparte por nodos, fragmentos o réplicas, puedes probar <a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/serverless">Elastic Cloud Serverless</a>. Esta oferta de Elastic Cloud está completamente gestionada por Elastic y automatizada para escalar con tu carga de trabajo. Una prueba gratis puede ayudarte a familiarizarte con otros beneficios del enfoque sin servidor.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-shards-and-replicas-guide</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-shards-and-replicas-guide</guid>
    <category><![CDATA[Conceptos básicos]]></category>
    <dc:creator><![CDATA[Piotr Przybyl]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt71dd92d939d383a2/6a17e9d53e03d769c44f2cc7/7775c44f01f2516c4ff4cce6d6bbe9e7b2c38908-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 14 Aug 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Probar tu código Java con mocks y Elasticsearch real]]></title>
    <description><![CDATA[Aprende a escribir tus pruebas automatizadas para Elasticsearch, usando mocks y Testcontainers]]></description>
    <content:encoded><![CDATA[<p>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.</p><h2>Buenas pruebas para la confianza del sistema</h2><p>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:</p><ul><li><p>Lo que queremos entregar va a funcionar en producción.</p></li><li><p>El sistema cumple con los requisitos y los contratos.</p></li><li><p>No habrá regresiones en el futuro.</p></li><li><p>Los desarrolladores (y otros miembros del equipo implicados) están seguros de que lo que crearon funcionará.</p></li></ul><p>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.</p><p>Unas buenas pruebas automatizadas hacen que los distintos miembros del equipo se sientan más seguros:</p><ul><li><p>Desarrolladores: pueden confirmar que lo que hacen funciona (incluso antes de que el código en el que trabajan salga de su máquina).</p></li><li><p>Equipo de control de calidad: tienen menos que probar manualmente.</p></li><li><p>Los operadores de sistemas y los SRES: son más relajados, porque los sistemas son más fáciles de desplegar y mantener.</p></li></ul><p>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.</p><h2>Dos tipos de pruebas: Mocks y dependencias</h2><p>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.</p><p>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.</p><p>Sin embargo, introducir simulacros tiene varias participaciones:</p><ul><li><p>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).</p></li><li><p>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.</p></li><li><p>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.</p></li><li><p>Se necesita disciplina para cerciorar que el esfuerzo se destine a desarrollar y probar el sistema, no a los simulacros.</p></li></ul><p>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.</p><p>Sin embargo, cuando las pruebas emplean únicamente dependencias reales, debemos considerar los siguientes aspectos:</p><ul><li><p>Algunos escenarios de prueba no necesitan la dependencia real (por ejemplo, para verificar los invariantes estáticos de una petición).</p></li><li><p>Estas pruebas normalmente no se ejecutan en suites completas en las máquinas de los desarrolladores, porque esperar retroalimentación llevaría demasiado tiempo.</p></li><li><p>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.</p></li><li><p>Puede que no sea trivial inicializar dependencias con datos de prueba.</p></li><li><p>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.</p></li><li><p>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.</p></li></ul><h2>El punto óptimo: usa ambas pruebas</h2><p>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.</p><ul><li><p>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.</p></li><li><p>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.</p></li><li><p>No hay nada de malo en probar un fragmento de código usando ambos enfoques, siempre que tenga sentido.</p></li></ul><h2>Ejemplo de SystemUnderTest</h2><p>Para las siguientes secciones vamos a usar un ejemplo que se puede encontrar <a href="https://github.com/pioorg/testing-elasticsearch">aquí</a>. 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 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html">ES|QL</a> (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.</p><p>El <code>BookSearcher</code> nos ayuda a gestionar la búsqueda y el análisis de datos, siendo en nuestro caso los libros (como se demostró en <a href="https://www.elastic.co/search-labs/blog/esql-queries-to-java-objects">una de las entradas anteriores</a>).</p><ul><li><p>Requiere Elasticsearch exactamente en la versión <code>8.15.x</code> como su única dependencia (ver <code>isCompatibleWithBackend()</code>), 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.</p></li><li><p>Podemos usarlo para buscar el número de libros publicados en un año determinado (ver <code>numberOfBooksPublishedInYear</code>).</p></li><li><p>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 <code>mostPublishedAuthorsInYears</code>).</p></li></ul>public class BookSearcher {

    private final ElasticsearchClient esClient;

    public BookSearcher(ElasticsearchClient esClient) {
        this.esClient = esClient;
        if (!isCompatibleWithBackend()) {
            throw new UnsupportedOperationException("This is not compatible with backend");
        }
    }

    private boolean isCompatibleWithBackend() {
        try (ResultSet rs = esClient.esql().query(ResultSetEsqlAdapter.INSTANCE, """
            show info
            | keep version
            | dissect version "%{major}.%{minor}.%{patch}"
            | keep major, minor
            | limit 1""")) {
            if (!rs.next()) {
                throw new RuntimeException("No version found");
            }
            return rs.getInt(1) == 8 &amp;&amp; rs.getInt(2) == 15;
        } catch (SQLException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    public int numberOfBooksPublishedInYear(int year) {
        try (ResultSet rs = esClient.esql().query(ResultSetEsqlAdapter.INSTANCE, """
            from books
            | where year == ?
            | stats published = count(*) by year
            | limit 1000""", year)) {

            if (rs.next()) {
                return rs.getInt("published");
            }
        } catch (SQLException | IOException e) {
            throw new RuntimeException(e);
        }
        return 0;
    }


    public List&lt;MostPublished&gt; mostPublishedAuthorsInYears(int minYear, int maxYear) {
        assert minYear &lt;= maxYear;
        String query = """
            from books
            | where year &gt;= ? and year &lt;= ?
            | stats first_published = min(year), last_published = max(year), times = count (*) by author
            | eval years_published = last_published - first_published
            | sort years_published desc
            | drop years_published
            | limit 20
            """;

        try {
            Iterable&lt;MostPublished&gt; published = esClient.esql().query(
                ObjectsEsqlAdapter.of(MostPublished.class),
                query,
                minYear,
                maxYear);

            List&lt;MostPublished&gt; mostPublishedAuthors = new ArrayList&lt;&gt;();
            for (MostPublished mostPublished : published) {
                mostPublishedAuthors.add(mostPublished);
            }
            return mostPublishedAuthors;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public record MostPublished(
        String author,
        @JsonProperty("first_published") int firstPublished,
        @JsonProperty("last_published") int lastPublished,
        int times
    ) {
        public MostPublished {
            assert author != null;
            assert firstPublished &lt;= lastPublished;
            assert times &gt; 0;
        }
    }
}
<h2>Prueba con simulacros para empezar</h2><p>Para crear los mocks usados en nuestros exámenes vamos a usar <a href="https://site.mockito.org/">Mockito</a>, una biblioteca de mocking muy popular en el ecosistema Java.</p><p>Podríamos empezar con lo siguiente, para que los simulacros se resetear antes de cada examen:</p>public class BookSearcherMockingTest {

    ResultSet mockResultSet;
    ElasticsearchClient esClient;
    ElasticsearchEsqlClient esql;

    @BeforeEach
    void setUpMocks() {
        mockResultSet = mock(ResultSet.class);
        esClient = mock(ElasticsearchClient.class);
        esql = mock(ElasticsearchEsqlClient.class);

    }
}
<p>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 <code>8.15.x</code> por ahora (en el futuro podríamos ampliar el rango una vez confirmemos que nuestro sistema es compatible con futuras versiones):</p>@Test
void canCreateSearcherWithES_8_15() throws SQLException, IOException{
    // when
    when(esClient.esql()).thenReturn(esql);
    when(esql.query(eq(ResultSetEsqlAdapter.INSTANCE), anyString())).thenReturn(mockResultSet);
    when(mockResultSet.next()).thenReturn(true).thenReturn(false);
    when(mockResultSet.getInt(1)).thenReturn(8);
    when(mockResultSet.getInt(2)).thenReturn(15);

    // then
    Assertions.assertDoesNotThrow(() -&gt; new BookSearcher(esClient));
}
<p>Podemos verificar de forma similar (simplemente devolviendo una versión menor diferente) que nuestro <code>BookSearcher</code> aún no va a funcionar con <code>8.16.x</code> , porque no estamos seguros de si será compatible con él:</p>@Test
void cannotCreateSearcherWithoutES_8_15() throws SQLException, IOException {
    // when
    when(esClient.esql()).thenReturn(esql);
    when(esql.query(eq(ResultSetEsqlAdapter.INSTANCE), anyString())).thenReturn(mockResultSet);
    when(mockResultSet.next()).thenReturn(true).thenReturn(false);
    when(mockResultSet.getInt(1)).thenReturn(8);
    when(mockResultSet.getInt(2)).thenReturn(16);

    // then
    Assertions.assertThrows(UnsupportedOperationException.class, () -&gt; new BookSearcher(esClient));
}
<p>Ahora veamos cómo podemos lograr algo similar al probar contra un Elasticsearch real. Para esto vamos a usar <a href="https://java.testcontainers.org/modules/elasticsearch/">el módulo Elasticsearch de Testcontainers</a>, 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.</p><p>El artículo puede ver así:</p>@Testcontainers
public class BookSearcherIntTest {

    static final String ELASTICSEARCH_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch:8.15.0";
    static final JacksonJsonpMapper JSONP_MAPPER = new JacksonJsonpMapper();

    RestClientTransport transport;
    ElasticsearchClient client;

    @Container
    ElasticsearchContainer elasticsearch = new ElasticsearchContainer(ELASTICSEARCH_IMAGE);

    @BeforeEach
    void setupClient() {
        transport = // setup transport here
        client = new ElasticsearchClient(transport);
    }

    @AfterEach
    void closeClient() throws IOException {
        if (transport != null) {
            transport.close();
        }
    }

}
<p>En este ejemplo dependemos de <a href="https://java.testcontainers.org/test_framework_integration/junit_5/">la integración de JUnit de Testcontainers</a> con <code>@Testcontainers</code> y <code>@Container</code>, 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).</p><p>Anotar un campo no estático con <code>@Container</code> 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.</p><p><strong>Nota:</strong></p>Al depender de <code>docker.elastic.co</code> (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 etiqueta <code>latest</code> para las imágenes de Elasticsearch.<h2>Conexión con Elasticsearch en pruebas</h2><p><a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html">El cliente Java de Elasticsearch</a> 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.</p><p>Cómo obtener los datos necesarios para la conexión, suponiendo que el contenedor esté asignado a campo o variable <code>elasticsearch</code>:</p><ul><li><p><code>elasticsearch.getHost()</code> Te dará el host en el que se ejecuta el contenedor (que la mayoría de las veces probablemente será <code>"localhost"</code>, 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).</p></li><li><p><code>elasticsearch.getMappedPort(9200)</code> 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).</p></li><li><p>A menos que fueron sobreescribir por defecto, el nombre de usuario y la contraseña por defecto son <code>"elastic"</code> y <code>"changeme"</code> respectivamente.</p></li><li><p>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 <a href="https://curl.se/docs/manpage.html#--cacert">puede hacer cURL</a>) el certificado puede obtener usando <code>elasticsearch.caCertAsBytes()</code> (que devuelve <code>Optional&lt;byte[]&gt;</code>), u otra forma conveniente es obtener <code>SSLContext</code> usando <code>createSslContextFromCa()</code>.</p></li></ul><p>El resultado general podría ser el siguiente:</p>BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("elastic", "changeme"));

// Create a low level rest client
RestClient restClient = RestClient.builder(new HttpHost(elasticsearch.getHost(), elasticsearch.getMappedPort(9200), "https"))
    .setHttpClientConfigCallback(httpClientBuilder -&gt;
        httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
            .setSSLContext(elasticsearch.createSslContextFromCa())
    )
    .build();

// The RestClientTransport is mainly for serialization/deserialization
RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());

// The official Java API Client for Elasticsearch
ElasticsearchClient client = new ElasticsearchClient(transport);
<p>Otro ejemplo de creación de una instancia de <code>ElasticsearchClient</code> se puede encontrar en el <a href="https://github.com/pioorg/testing-elasticsearch/blob/e800b4b2ab3d706efcafb9a8182480e69e475b86/src/test/java/testing_elasticsearch/BookSearcherIntTest.java#L61">proyecto demo</a>.</p><p><strong>Nota</strong>:</p>Para crear clientes en entornos de producción, consulte <a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/connecting.html#_verifying_https_with_a_certificate_fingerprint">la documentación</a>.<h2>Primera prueba de integración</h2><p>Nuestra primera prueba, verificando que podemos crear <code>BookSearcher</code> usando Elasticsearch versión 8.15.x, podría ser así:</p>@Test
void canCreateClientWithContainerRunning_8_15() {
    Assertions.assertDoesNotThrow(() -&gt; new BookSearcher(client));
}
<p>Como puedes ver, no necesitamos montar nada más. No necesitamos simular la versión devuelta por Elasticsearch, lo único que necesitamos es proporcionar <code>BookSearcher</code> le un cliente conectado a una instancia real de Elasticsearch, que fue iniciada para nosotros por Testcontainers.</p><h2>A las pruebas de integración les importan menos los componentes internos</h2><p>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 <code>isCompatibleWithBackend</code> en lugar de</p>return rs.getInt(1) == 8 &amp;&amp; rs.getInt(2) == 15;
<p>Vamos a tener:</p>return rs.getInt("major") == 8 &amp;&amp; rs.getInt("minor") == 15;
<p>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 <code>rs.getInt(int)</code>, no <code>rs.getInt(String)</code>. Para que pasen, ahora tenemos que simularlos o simularlos a ambos, dependiendo de otros casos de uso que tengamos en nuestro conjunto de pruebas.</p><h2>Las pruebas de integración pueden ser un cañón para matar a una mosca</h2><p>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 <code>mostPublishedAuthorsInYears(int minYear, int maxYear)</code>. Las dos primeras líneas son las siguientes:</p>assert minYear &lt;= maxYear;
String query = // here goes the query
<p>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 <code>minYear</code> es mayor que <code>maxYear</code>, se lanza una excepción.</p><p>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:</p>BookSearcher systemUnderTest = new BookSearcher(esClient);

Assertions.assertThrows(
    AssertionError.class,
    () -&gt; systemUnderTest.mostPublishedAuthorsInYears(2012, 2000)
);
<p>Iniciar una dependencia, en lugar de burlar, sería un desperdicio en <a href="https://github.com/pioorg/testing-elasticsearch/blob/e800b4b2ab3d706efcafb9a8182480e69e475b86/src/test/java/testing_elasticsearch/BookSearcherMockingTest.java#L89">este caso de prueba</a> porque no hay posibilidad de tomar una decisión significativa para esta dependencia.</p><p>Sin embargo, para verificar el comportamiento que empieza por <code>String query = ...</code>, 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:</p>@BeforeEach
void setupDataInContainer() {
    // here we initialise data in the Elasticsearch running in a container
}

@Test
void shouldGiveMostPublishedAuthorsInGivenYears() {
    var systemUnderTest = new BookSearcher(client);
    var list = systemUnderTest.mostPublishedAuthorsInYears(1800, 2010);
    Assertions.assertEquals("Beatrix Potter", list.get(12).author(), "Beatrix Potter was 13th most published author between 1800 and 2010");
}
<p>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, <code>8.15</code> cambiaría esto:</p>static final String ELASTICSEARCH_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch:8.15.0";
<p>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.</p><h2>Emplea ambos enfoques cuando sea necesario</h2><p>El caso del método <code>mostPublishedAuthorsInYears</code> ilustra que un solo método puede probar usando ambos métodos. Y quizá incluso debería estarlo.</p><ul><li><p>Usar solo mocks significa que tenemos que mantener el mock y no tener ninguna confianza al actualizar nuestro sistema.</p></li><li><p>Usar solo pruebas de integración significaría que estamos desperdiciando bastantes recursos, sin necesidad de ellos en absoluto.</p></li></ul><h2>Vamos a recapitular</h2><ul><li><p>Es posible usar tanto pruebas de simulación como de integración con Elasticsearch.</p></li><li><p>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 <code>./mvnw test '-Dtest=!TestInt*' &amp;&amp; ./mvnw test '-Dtest=TestInt*'</code> o <a href="https://maven.apache.org/surefire/maven-failsafe-plugin/">plugins Failsafe</a> y <a href="https://maven.apache.org/surefire/maven-surefire-plugin/">Surefire</a> ).</p></li><li><p>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).</p></li><li><p>Emplea pruebas de integración para verificar tus suposiciones e integración con sistemas externos.</p></li><li><p>No tengas miedo de probar usando ambos enfoques —si tiene sentido— según los puntos anteriores.</p></li></ul><p>Se podría observar que ser tan estricto con la versión (en nuestro caso <code>8.15.x</code>) 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.</p><p>En la <a href="https://www.elastic.co/search-labs/blog/automated-integration-tests-faster-elasticsearch">próxima entrega del serial</a>, 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 <a href="https://discuss.elastic.co/">foros de discusión</a> y <a href="https://communityinviter.com/apps/elasticstack/elastic-community">en el canal comunitario de Slack</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/tests-with-mocks-and-real-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/tests-with-mocks-and-real-elasticsearch</guid>
    <category><![CDATA[Java]]></category>
    <dc:creator><![CDATA[Piotr Przybyl]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7e4d003f09dfbe50/6a1709aba929cf810aae0957/b6bb727815ebdb844aeb36d5c44cdf3657f0e4bc-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 03 Oct 2024 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>