¡Compártelo!

Test Data Builder: Optimiza tus pruebas de software

«Suprimir la redundancia y mejorar la claridad en la generación de datos de prueba.» Imaginemos un mundo sin código repetitivo y con pruebas de software que son claras, legibles y expresivas. En este artículo explicaremos una técnica que nos acerca un paso más a ese ideal: Test Data Builder.

A medida que profundizamos en la escritura de test nos encontramos con un problema recurrente que no nos permite crear de forma sencilla nuestro juego de datos de prueba necesarios para cubrir todos los casos existentes dentro de nuestro código. 

Para ello presentaremos una forma muy sencilla de eliminar la duplicación en la construcción de objetos mediante el patrón Test Data Builder, permitiéndonos escribir pruebas más comprensibles y efectivas sin necesidad de que nuestro código de pruebas esté lleno de detalles que son irrelevantes para el comportamiento que estamos probando. 

En este post usaremos Kotlin para el código de ejemplo escribiendo todos los ejemplos de definición de clases en Java excepto el último permitiendo ver que  la solución de Kotlin es mucho más potente.

Construyendo datos de prueba

Imaginemos que tenemos una clase Order con la siguiente estructura dentro de nuestro código:

test data builder
duplicado creado con Test Data Builder usando Lombok

Dentro de nuestro test necesitamos crear tres órdenes diferentes :

  • order: tiene la referencia «referencia» y el nombre «nombre».
  • order2: toma la referencia de order, que es «referencia», y tiene un nombre diferente, que es «nombre2».
  • order3: tiene una referencia diferente, «referencia2», y comparte el nombre con order, que es «nombre».
Test Data Builder
Test Data Builder

La prueba anterior muestra la creación de objetos Order con diferentes valores. Como se puede observar el test  incluye muchos detalles innecesarios relacionados con la creación de objetos. Si introducimos un nuevo campo o hacemos cambios en el constructor, tendríamos que modificar muchas líneas de código en la prueba, lo que la hace fuertemente acoplada a cómo se crean los objetos. Esto puede llevar a escribir pruebas frágiles poco resilientes a los cambios dentro de nuestra aplicación.

Facilitando la creación de objetos con el uso de Test Data Builder

El patrón Test Data Builder es un diseño ampliamente utilizado en programación orientada a objetos con el propósito de ofrecer una solución flexible a los desafíos relacionados con la creación de objetos complejos. Su objetivo principal es separar el proceso de construcción de un objeto complejo de su representación.

En escenarios donde las clases requieren una configuración detallada y complicada, la implementación de un generador de datos de prueba, o «builder,» se convierte en una práctica común. Este «Test Data Builder» se encarga de definir y establecer los valores para cada uno de los parámetros del constructor de la clase, simplificando así el proceso de creación y configuración de objetos complejos en entornos de prueba.

uso del Test Data Builder

Como podemos observar hemos creado un método “withxxx” cada uno de los atributos que podemos modificar asignado valores por defecto creados con la librería Instancio de Java.

Instancio es una biblioteca que se utiliza para simplificar la creación de objetos en pruebas unitarias y para la generación de datos de prueba. Facilita la creación de objetos complejos, como los que se utilizan en las pruebas, al proporcionar un enfoque más conciso y legible para configurar los valores de los atributos de esos objetos.

En lugar de crear manualmente instancias de objetos y definir explícitamente cada uno de sus atributos, Instancio permite definir un objeto de la clase deseada con sus valores iniciales de manera más sencilla.

Instancio Test Data Builder

Ventajas de usar Test Data Builder

Las principales ventajas de este nuevo enfoque usando Test Data Builder son:

  • El código de prueba se enfoca únicamente en los atributos relevantes para el caso de prueba.
  • Permite asignar valores solo a los atributos necesarios, lo que mejora la claridad del código.
  • La flexibilidad aumenta al crear objetos con diferentes configuraciones de atributos.
  • La fragilidad del código de prueba disminuye, ya que los cambios en la construcción del objeto únicamente  requieren ajustes en el Builder, no en todos los lugares donde se crea el objeto.
  • El código de prueba se vuelve más legible y expresivo, ya que cada método en el Builder se encarga de asignar un valor específico a un atributo, facilitando la comprensión.

Aún así queda un aspecto del código que podemos mejorar, que consiste en la creación de métodos with de manera manual para cada uno de los atributos que queremos poder modificar dentro del builder creado.

Reduciendo el código duplicado creado con Test Data Builder usando Lombok

Aunque el patrón Test Data Builder  implementado para la generación de datos de prueba proporciona muchos beneficios, también tiene un  principal inconveniente importante: el desarrollador termina escribiendo mucho código redundante.

Para abordar este problema de código redundante, podemos aprovechar el uso de la Lombok. Podemos eliminar el constructor por defecto, los métodos getter y crear automáticamente una clase generadora de los métodos with mediante la anotación de la clase con las anotaciones Lombok @NoArgsConstructor , @AllArgsConstructor y

@With

Veamos a continuación un ejemplo de cómo quedaría una nueva clase Product usando el patrón builder con el apoyo de la librería Lombok

Lombok Test Data Builder
duplicado creado con Test Data Builder usando Lombok

La clase Products se anota con las anotaciones de Lombok NoArgsConstructor, AllArgsConstructor y With, que ayudan a generar automáticamente constructores sin argumentos, constructores con todos los argumentos y métodos para clonar objetos con campos modificados de manera análoga a los with creados en el capítulo anterior pero sin necesidad de escribir el código repetitivo a mano.

Bonus track: La clase de Producto está anotada con @with de Lombok, lo que permite copiar objetos inmutables creados utilizando instancias de una clase definida como Record en Java. (tenemos un método with para cada uno de los atributos)

Pasamos ahora a ver el ejemplo de un test creado para dicha clase:

test Lombock

Reduciendo el código de Test Data Builder usando Kotlin

Para aquellos que no conocen Kotlin lo podemos resumir como el Java del año 2050. Creado por Jetbrains bajo la necesidad de escribir aplicaciones que corran dentro de la JVM con la potencia de lenguajes más funcionales y menos verbosos que Java pero sin la complejidad de aprendizaje de lenguajes como Scala o similares. En Profile lo usamos como JDK first siendo la elección a la hora de escribir aplicaciones en nuestros backend creados con Spring. Veamos el patrón de construcción de objetos en Kotlin

Para ello creamos una clase Usuario con un data class (análogo a los Record de jdk 17)

Test usando Kotlin
Test Kotlin

Creamos la factoría del objeto User  usando el enfoque Test Data Builder (aprovechamos el uso que permite Kotlin de administrar valores por defecto dentro de clases/funciones para poder dar a nuestro builder los valores creados por Instancio).

Test usando Kotlin

Para poder utilizar de nuestra factoría de clases hacemos uso de otra funcionalidad de Kotlin que nos permite pasar valores a las funciones con el nombre del atributo únicamente pasando aquellos que necesitamos sobreescribir a los generados por Instancio

La construcción de objetos por nombre en Kotlin se basa en el uso de argumentos con nombre («named arguments» / «named parameters”). Cuando defines una función en Kotlin, puedes asignar un nombre a cada parámetro de la función. Luego, al llamar a la función, puedes proporcionar los argumentos en cualquier orden utilizando el nombre del parámetro correspondiente.

Test Data Builder usando Kotlin

En este caso llamamos a la función build sobrescribiendo el valor del atributo name definido creado por Instancio asignado el valor “cristian”.

val user2 = Users.build(name = «cristian»)

Test usando Kotlin

En este otro caso llamamos a la función build sobrescribiendo el valor del atributo name por “jose”  y surname definido creado por Instancio por “gomez”.

val user3 = Users.build(name = «jose», surname = «gomez»)

Test usando Kotlin

Para este otro valor hacemos el uso copy del método de las data class de kotlin que permite copiar el objeto completo y cambiar aquellas propiedad que definamos dentro (Ojalá existiera en los Record de java). En este caso modificamos el atributo id asociado al user3 creado con el valor 33.

val userWithNewId = user3.copy(id = «33»)

Test usando Kotlin

Conclusión

En este artículo, hemos explorado patrones para la creación de objetos en pruebas, lo que nos permite construir pruebas de manera más efectiva y expresiva. Hemos demostrado cómo aplicar estos patrones en Kotlin, un lenguaje que nos brinda poderosas herramientas para minimizar el código y mejorar la legibilidad. A medida que avanzamos en nuestros proyectos de desarrollo, espero que consideremos la aplicación de estos patrones para simplificar nuestras pruebas y, en última instancia, mejorar la calidad de nuestro software. ¡Gracias por seguir este recorrido y no dudes en experimentar con estas técnicas en tus propios proyectos

Código Ejemplo Github:

https://github.com/cristianprofile/test-factory-pattern

Artículos ​ relacionados