¿Cómo crear test automáticos para probar pipelines del Dataflow?

Introducción

El módulo Dataflow de Onesait Platform es una herramienta low-code que sirve para definir y ejecutar flujos de datos. En este artículo veremos cómo se puede utilizar JUnit para automatizar pruebas de flujos de datos.

Para probar los pipelines necesitamos una instancia del Dataflow donde ejecutarlos. Para mantener este test totalmente independiente de recursos externos, usamos la librería TestContainers para ejecutar una instancia de Dataflow en el propio test. Sería más eficiente y rápido a la hora de ejecutar los tests contar con una instancia dedicada con todos los pipelines que se necesiten probar.

El objetivo de este ejemplo es mostrar cómo se puede realizar este tipo de tests, y por lo tanto el pipeline que vamos a probar es muy sencillo. Cualquier otro pipeline se probaría de forma similar.

La estrategia que sigue este ejemplo es utilizar la librería cliente del dataflow para gestionar los flujos de una instancia, ejecutar una preview de forma remota y validar los valores de los records en cada una de las etapas. Es decir, lo mísmo que haríamos de forma manual y visual al usar la preview de un pipeline, pero automatizado.

Se podrían realizar otro tipo de tests, como la ejecución completa de un pipeline y validar que el número de records en las salidas y el número y tipo de errores es el esperado, pero la información que proporciona ese tipo de test es menos precisa.

El código completo de este ejemplo está en github.

Configuración de la instancia de Dataflow

Como se ha comentado anteriormente en este ejemplo se está usando la librería TestContainers, que permite levantar y parar contenedores de forma automática en una prueba.

Para utilzarla con JUnit 5 necesita las siguientes dependencias:

<dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <version>1.18.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <version>1.18.3</version> <scope>test</scope> </dependency>

Configurar el contenedor de forma básica es muy sencillo, basta con anotar la clase del test con @Testcontainers y crear un atributo con la anotación @Container. Por ejemplo, para nuestro caso:

private static final String IMAGE = "registry.onesaitplatform.com/onesaitplatform/streamsets:4.1.0-overlord-323"; private static final int PORT = 18630; @Container public static GenericContainer<?> dataflow = new GenericContainer<>(IMAGE) .withExposedPorts(PORT) .withStartupTimeout(Duration.ofSeconds(120));

La librería Testcontainer automáticamente creará y destruirá un contenedor para cada test que se ejecute. Esto puede ralientizar la ejecución de los tests. Además cada nuevo contenedor se ejecutará sin pipelines ni librerías adicionales instaladas por lo que habrá que hacerlo en el propio proceso de test. Existen varias estrategias para solventar esto:

  • Por un lado se puede tener una imagen configurada con todos los pipelines y librerías requeridas instaladas. Esto es tan sencillo como crear una imagen a partir de un contenedor existente que sí se haya configurado con el comando docker commit <container> <new_image_name> .

  • Por otro lado se puede tener una instancia del dataflow lista y configurada en un entorno para que se utilice en la ejecución de tests.

Test con JUnit

Creción del cliente para el Dataflow

Lo primero que hace falta para ejecutar los test es crear un instancia del cliente para el dataflow

ApiClient apiClient = new ApiClient(authType); apiClient.setUserAgent("SDC CLI"); apiClient.setBasePath(getUrl(port) + "/rest"); apiClient.setUsername(user); apiClient.setPassword(password); SimpleModule module = new SimpleModule(); module.addDeserializer(FieldJson.class, new FieldDeserializer()); apiClient.getJson().getMapper().registerModule(module);

Además de crear el cliente en el código se puede ver que se está registrando un Deserializador. Esto no es obligatorio. En este caso se utiliza para usar las clases Field del Dataflow para facilitar la consulta de los valores de los Records, pero se podrían comprobar utilizando el valor en Json original.

El código de creación de un cliente está encapsulado en una clase de utilidad para que pueda reutilizarse en varios tests github.

Importamos el pipeline a probar

Lo siguiente que hacemos es importar el pipeline a probar.

Ver la sección Configurar Dataflow, para ver cómo configurar escenarios más complejos.

El código de importación de un pipeline está encapsulado en una clase de utilidad para que pueda reutilizarse en varios tests github.

Ejecutamos una preview del pipeline

Una vez tenemos el pipeline en la instancia del dataflow, vamos a ejectuar una preview del pipeline.

El siguiente fragmente de código hace exactamente eso. Ejecuta una preview, espera a que el resultado esté listo y obtiene los datos de la preview.

El código de ejecución de una preview de un pipeline está encapsulado en una clase de utilidad para que pueda reutilizarse en varios tests github.

Validación del resulado de la preview

Una vez tenemos la preview ejecutada podemos pasar a analizar los resultados y determinar si son los esperados. Básicamente esto cosisten en comprobar batch a batch si en las etapas que nos interese los records tienen los valores esperados. Es decir, que se obtienen los datos que se esperaban y que se hacen las transformaciones que se esperaban antes de enviar los datos al destino.

El ejemplo que hemos incluido sólo genera un batch de datos. En la mayoría de los casos con esto será sufiente. Si se requiren pruebas con varios batches, es recomendable usar un número manejable de los mismos para simplificar la validación de los resultados. Para cada batch que se obtenga habrá un conjunto de Records en cada una de las etapas que forman el pipeline. El test deberá comprobar que los valores se corresponden con lo que se espera en dicha etapa.

El pipeline de este ejemplo se corresponede con el de la siguiente imagen:

Pipeline de ejemplo para tests

Los nombres DevRawDataSource_01 y Trash_01 usados en el bloque case de la captura de código anterior para determinar la etapa de ejecución, se pueden ver en la información de cada una de las etapas en el editor de pipelines:

Ejemplo de nombre de etapa en un pipeline