Realizando tests de integración con Elasticsearch

0
2595

En este tutorial veremos cómo desarrollar pruebas cuando construimos componentes que interaccionan con Elasticsearch gracias al componente de testing que nos ofrecen desde el equipo de Elastic.

Índice de contenidos


1. Introducción

Como ya sabéis el testing es algo esencial en el desempeño día a día de nuestro trabajo como desarrolladores, ya hemos hablado en diversas ocasiones sobre su importancia.

Si bien ya existe algún tutorial sobre el testing de elasticsearch, como este de Daniel Rodríguez, en este tutorial veremos como desarrollar pruebas cuando construimos componentes que interaccionan con Elasticsearch gracias al componente de testing que no ofrecen desde el equipo de Elastic.

Este componente, utilizado internamente en Elastic para el desarrollo de Elasticsearch, permite configurar infraestructuras de cluster formadas por múltiples nodos facilitándonos el camino para la codificación de las pruebas que validen la existencia de errores en nuestro código.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro 17′ (2.66 GHz Intel Core i7, 8GB DDR3 SDRAM).
  • Sistema Operativo: Mac OS Sierra 10.12.5
  • Entorno de desarrollo: IntelliJ 2017.1
  • Apache Maven 3.3.9


3. Dependencias

Lo primero que tendremos que hacer para poder hacer uso de esta biblioteca es declarar las dependencias del mismo.

En este caso, procederemos sobre una aplicación gestionada mediante maven, con lo que definiremos dichas dependencias en el fichero pom.xml.

pom.xml
<dependencies>
	<dependency>
	  <groupId>org.apache.lucene</groupId>
	  <artifactId>lucene-test-framework</artifactId>
	  <version>${lucene.version}</version>
	  <scope>test</scope>
	</dependency>
	<dependency>
	  <groupId>org.elasticsearch.test</groupId>
	  <artifactId>framework</artifactId>
	  <version>${elasticsearch.version}</version>
	  <scope>test</scope>
	</dependency>
</dependencies>


4. Tests de integración

4.1. Ejemplo básico de test

A continuación mostramos el código fuente de un test de integración muy sencillo que nos servirá a modo de ejemplo sobre los puntos que necesitamos cumplir para crear nuestros test de integración.

ExampleElasticSearchIT.java
public class ExampleElasticSearchIT extends ESIntegTestCase {

  @Test
  public void someExampleTest() {
      createIndex("test");
      ensureGreen();

      client().prepareIndex("test", "type", "1").setSource("field", "xxx").execute().actionGet();
      refresh();

      SearchResponse searchResponse = client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery()).execute().actionGet();

      assertNoFailures(searchResponse);
      assertFirstHit(searchResponse, hasId("1"));
  }
}

Haciendo que nuestros tests hereden de ESIntegTestCase no tendremos que preocuparnos por montar la infraestructura necesaria para dar soporte a nuestros tests puesto que será la propia biblioteca la que se encargue de proveernos de todo lo necesario.

En el snipet de código anterior se muestra cómo crear un índice (createIndex(«test»);) y asegurarnos de que es correcto (ensureGreen();) a través de un cliente (client()) se dará de alta un nuevo documento dentro del índice con un mapping concreto:

client().prepareIndex(«test», «type», «1»).setSource(«field», «xxx»).execute().actionGet();

Forzaremos la disponibilidad de la información para las búsquedas (refresh();) y posteriormente realizamos una petición de búsqueda:

SearchResponse searchResponse =

client().prepareSearch(«test»).setQuery(QueryBuilders.matchAllQuery()).execute().actionGet();

Para finalizar realizaremos las aserciones correspondientes que validen nuestro código.


4.2. Configuración

El grado de configuración de la infraestructura de pruebas dependerá de nosotros en todo momento, pudiendo establecer cualquier tipo de particularidad necesaria para nuestros tests.

La configuración «out-of-the-box» genera un cluster que dispondrá de Scope SUITE, entre 1 y 3 o 6 nodos de datos (dependiendo del tipo de ejecución), entre 1 y 3 nodos maestros y 1 nodo coordinador.

Los distintos Scope (SUITE o TEST) marcarán el ámbito para el cual se va a crear el cluster y se pueden configurar mediante la anotación @ClusterScope(scope=SUITE).

  • SUITE: Se creará un cluster nuevo por suite de tests, borrando todos los índices y templates entre cada test.
  • TEST: Se creará un cluster nuevo por cada test.

Esta anotación también nos permite configurar otros aspectos relativos al cluster como pueden ser: número de nodos de datos, número máximo y mínimo de nodos de datos (por si queremos utilizar un valor aleatorio acotado), si damos soporte a nodos master dedicados, número de nodos cliente…

Podemos utilizar otras anotaciones existentes dentro de la biblioteca a nivel de test para establecer otras configuraciones, como son:

  • @Nightly: Establece que el test se ejecuta solo en construcciones nigthly. Esta anotación tiene consecuencias a nivel de configuración del cluster .
  • @Backwards: Establece que se trata de un test de retrocompatibilidad.
  • @AwaitsFix: Establece que el test esta pendiente de resolver algún bug.
  • @BadApple: Establece que el test falla de manera random.

Si bien en el ejemplo del punto anterior hemos decidido que la biblioteca nos devuelva un cliente conectado a un nodo aleatorio (a través de client()), dentro InternalTestCluster disponemos de un amplio conjunto de métodos para recuperar clientes conectados a nodos dedistinta naturaleza dentro del cluster:

  • client()
  • coordOnlyNodeClient()
  • dataNodeClient()
  • masterClient()
  • nonMasterClient()
  • smartClient()
  • transportClient()


4.3. Helpers

La clase ESIntegTestCase también nos proveerá de una serie de metodos (helpers) que nos serán de gran utilidad en nuestros tests. El conjunto de helpers se puede categorizar de acuerdo a la funcionalidad que otorga pudiendo encontrar aserciones, métodos de configuración del cluster, métodos de explotación del cluster, etc. Si bien la documentación de estos helpers puede consultarse aquí, a continuación veremos en detalle algunos de ellos:

  • refresh(): dispara el proceso de refresco de los índices que existan en el cluster.
  • waitForDocs(numDocs): espera hasta que un número dado de documentos (numDocs) se encuentra visible para búsquedas.
  • ensureYellow() y ensureGreen(): comprueba el estado del cluster y si no es Amarillo o Verde, según el caso, lanza un error de tipo AssertionError.
  • waitNoPendingTasksOnAll(): espera hasta que no haya tareas pendientes en ningún nodo.
  • waitForRelocation(): espera hasta que todos los shards reubicados se encuentren activos.

Gracias a la jerarquía establecida, también dispondremos de los métodos helpers que exponen ESTestCase y LuceneTestCase.


4.4. Aserciones

Como en el caso de los Helpers, al extender la funcionalidad de ESIntgeTestCase tendremos acceso a una serie de asserts que pueden facilitarnos la generación de nuestros tests. Algunos ejemplos pueden ser:

  • assertAllShardsOnNodes(index, pattern): esta aserción comprueba que todos los shards se asignan a nodos con un patrón dado.
  • assertResultsAndLogOnFailure(expectedResults, searchResponse): comprueba que el número de documentos devueltop es el esperado, en caso contrario deja un log con la información.
  • assertPathHasBeenCleared(path): comprueba que el path especificado no contiene ningún fichero.

Como en el punto anterior, la jerarquía establecida nos añade todas las aserciones que se exponen tanto en ESTestCase y LuceneTestCase


5. Caso de uso

Para mostrar un caso no tan trivial como el del ejemplo en el punto 4.1.

BookSearchIT.java
public class BookSearch {

    private static final String TYPE_BOOK = "book";
    private static final String INDEX = "resources";
    private static final Logger LOGGER = ESLoggerFactory.getLogger(BookSearch.class);
    private final Client client;

    public BookSearch(Client client) {
        this.client = client;
    }

		public void createType() {
    this.client.admin().indices().prepareCreate(INDEX).get();
    this.client.admin().indices().preparePutMapping(INDEX)
            .setType(TYPE_BOOK)
            .setSource("{\n" +
                    "  \"properties\": {\n" +
                    "    \"author\": {\n" +
                    "      \"type\": \"string\"\n," +
                    "      \"index\": \"analyzed\"\n," +
                    "      \"store\": \"true\"\n" +
                    "    },\n" +
                    "    \"title\": {\n" +
                    "      \"type\": \"string\"\n," +
                    "      \"index\": \"analyzed\"\n," +
                    "      \"store\": \"true\"\n" +
                    "    },\n" +
                    "    \"recap\": {\n" +
                    "      \"type\": \"string\"\n," +
                    "      \"index\": \"analyzed\"\n," +
                    "      \"store\": \"true\"\n" +
                    "    }\n" +
                    "  }\n" +
                    "}", XContentType.JSON)
            .get();
}

    public void add(Book book) {
        IndexRequest request = new IndexRequest(INDEX, TYPE_BOOK, book.getId());
        request.source(book.toMap());
        IndexResponse response = this.client.index(request).actionGet();
        LOGGER.info(response.toString());
        LOGGER.info("entry added to index '" + response.getIndex() + "', type '" + response.getType() + "', doc-version: '" + response.getVersion() + "', doc-id: '" + response.getId() + "', result: " + response.getResult().toString() + "\n");
    }

    public List findByAuthor(String author) {
        SearchResponse response = this.client.prepareSearch()
                .setIndices(INDEX)
                .setTypes(TYPE_BOOK)
                .setQuery(matchQuery("author",author))
                .get();
        LOGGER.info(response.toString());
        SearchHits hits = response.getHits();
        LOGGER.info(hits.totalHits() + " hits found\n");

        return StreamSupport.stream(hits.spliterator(), true).map(hit -> {
            LOGGER.info("hit:" + hit.toString());
            Map fields = hit.getSource();
            String title_hit = (String) fields.get("title");
            String author_hit = (String) fields.get("author");
            String recap_hit = (String) fields.get("recap");
            return new Book(hit.getId(), author_hit, title_hit, recap_hit);
        }).collect(Collectors.toList());
    }
}
BookSearchIT.java
@ClusterScope(scope = Scope.SUITE)
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
public class BookSearchIT extends ESIntegTestCase {

    private static Client client;
    private BookSearch bookSearch;

    @Before
    public void init() throws Exception {
        super.setUp();
        client = dataNodeClient();
        bookSearch = new BookSearch(client);
        bookSearch.createType();

        bookSearch.add(new Book("1", "Luis Cernuda Bidou", "Los placeres prohibidos", "Libro de poemas publicado en 1931"));
        bookSearch.add(new Book("2", "Luis Cernuda Bidou", "Vivir sin estar viviendo", "Libro de poemas publicado en 1949"));
        bookSearch.add(new Book("3", "Rafael Alberti", "Marinero en tierra", "Libro de poemas publicado en 1925"));
        refresh("resources");
        indexExists("resources");
        ensureGreen("resources");
    }

    @Test
    public  void shouldIndexAndSearchBooks() throws Exception {
        List books = bookSearch.findByAuthor("Alberti");
        assertEquals(1, books.size());

        Book book = books.get(0);
        assertEquals(book.getTitle(), "Marinero en tierra");
    }

    @Test
    public  void shouldIndexAndSearchBooks2() throws Exception {
        List books = bookSearch.findByAuthor("Cernuda");
        assertEquals(2, books.size());
    }

}

6. Conclusiones

Como hemos visto, disponer de la infraestructura necesaria para realizar tests de integración de nuestros componentes de interacción de elasticsearch puede suponer una tarea ardua, sin embargo, gracias a la biblioteca de testing de Elastic, podemos lidiar con la mayoría de las situaciones de forma rápida y elegante.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad