Cómo Generar Datos para Pruebas Unitarias en JAVA con Instancio
Generación Eficiente de Datos para Pruebas Unitarias
Configurar datos en pruebas unitarias suele ser un proceso manual que involucra mucho código repetitivo. Esto es especialmente cierto al probar clases complejas que contienen muchos campos, relaciones y colecciones. Por lo general, los valores en sí mismos no son tan importantes como la mera presencia de un valor, lo cual se expresa normalmente con código como person.setName("test name")
. En este tutorial, analizaremos cómo Instancio puede ayudarnos a generar datos para pruebas unitarias mediante la creación de objetos completamente poblados. Cubriremos cómo se pueden crear, personalizar y reproducir objetos en caso de que falle una prueba.
Sobre Instancio
Instancio es un generador de datos de prueba diseñado para automatizar la configuración de datos en pruebas unitarias. Su objetivo principal es hacer que las pruebas unitarias sean más concisas y mantenibles al eliminar la configuración manual de datos tanto como sea posible. En pocas palabras, le proporcionamos a Instancio una clase, y ella nos proporciona un objeto completamente poblado lleno de datos generados aleatoriamente y reproducibles.
Dependencias de Maven
Primero, agreguemos la dependencia desde Maven Central. Dado que utilizaremos JUnit 5 para nuestros ejemplos, importaremos instancio-junit
:
<dependency>
<groupId>org.instancio</groupId>
<artifactId>instancio-junit</artifactId>
<version>2.9.0</version>
<scope>test</scope>
</dependency>
Alternativamente, la dependencia instancio-core
está disponible para usar Instancio de forma independiente, con JUnit 4 u otros marcos de prueba:
<dependency>
<groupId>org.instancio</groupId>
<artifactId>instancio-core</artifactId>
<version>2.6.0</version>
<scope>test</scope>
</dependency>
Generación de Objetos
Usando Instancio, podemos crear diferentes tipos de objetos, que incluyen:
- Valores simples, como Strings, fechas y números.
- POJOs regulares, incluidas las funciones Java.
- Colecciones, Maps y Streams.
- Tipos genéricos arbitrarios utilizando tokens de tipo.
Instancio utiliza valores predeterminados sensatos al poblar objetos. Los objetos generados tienen:
- Valores no nulos
- Strings no vacíos
- Números positivos
- Colecciones no vacías que contienen unos pocos elementos
El punto de entrada a la API son los métodos Instancio.create()
e Instancio.of()
. Usando estos métodos, podemos crear POJOs:
Student student = Instancio.create(Student.class);
Colecciones y Streams:
List<Student> list = Instancio.ofList(Student.class).size(10).create();
Stream<Student> stream = Instancio.of(Student.class).stream().limit(10);
Tipos genéricos arbitrarios usando un TypeToken
:
Pair<List, List
A continuación, veamos cómo se puede personalizar los datos generados.
Personalización de Objetos
Al escribir pruebas unitarias, a menudo necesitamos crear objetos en varios estados. El estado normalmente depende de la funcionalidad que se está probando. Por ejemplo, podemos necesitar un objeto válido para verificar el camino feliz y un objeto inválido para validar errores. Usando Instancio, podemos:
- Personalizar los valores generados según sea necesario a través de los métodos
set()
,supply()
, ygenerate()
. - Ignorar ciertos campos y clases usando el método
ignore()
. - Permitir que se generen valores nulos utilizando el método
withNullable()
. - Especificar implementaciones para tipos abstractos utilizando el método
subtype()
.
Selectores
Instancio utiliza selectores para especificar qué campos y clases personalizar. Todos los métodos enumerados anteriormente aceptan un selector como primer argumento. Podemos crear selectores usando métodos estáticos proporcionados por la clase Select
.
Por ejemplo, podemos seleccionar un campo particular usando una referencia de método, el nombre del campo o un predicado utilizando los siguientes métodos:
Select.field(Address::getCity)
Select.field(Address.class, "city")
Select.fields().matching("c.*y").declaredIn(Address.class) // coincide con ciudad, país
Select.fields(field -> field.getDeclaredAnnotations().length > 0)
También podemos seleccionar tipos especificando la clase o usando un predicado:
Select.all(Collection.class)
Select.types().of(Collection.class)
Select.types(klass -> klass.getPackage().getName().startsWith("com.example"))
Ejemplo de Personalización con set()
El método set()
se utiliza para establecer valores no aleatorios (esperados). Por ejemplo:
Student student = Instancio.of(Student.class)
.set(field(Phone::getCountryCode), "+49")
.create();
Una pregunta común es: ¿por qué no usar un setter normal después de que el objeto ha sido creado? A diferencia de un setter regular, el método set()
establece el código de país en todas las instancias de Phone
generadas.
Además, a veces trabajamos con clases inmutables, como los registros de Java. En tales casos, no sería posible modificar un objeto después de haber sido creado.
Usando supply()
El método supply()
tiene dos variantes: una para asignar valores no aleatorios utilizando un Supplier
y otra para generar valores aleatorios usando un Generator
.
El siguiente código ilustra ambas variantes:
Student student = Instancio.of(Student.class)
.supply(all(LocalDateTime.class), () -> LocalDateTime.now())
.supply(field(Student::getDateOfBirth), random -> LocalDate.now().minusYears(18 + random.intRange(0, 60)))
.create();
Usando generate()
Con el método generate()
, podemos personalizar valores a través de generadores de datos incorporados. Instancio proporciona generadores para los tipos más utilizados en Java. Esto incluye Strings, tipos numéricos, colecciones, arreglos, fechas, etc.
En el siguiente ejemplo, la variable gen
proporciona acceso a los generadores disponibles:
Student student = Instancio.of(Student.class)
.generate(field(Student::getEnrollmentYear), gen -> gen.temporal().year().past())
.generate(field(ContactInfo::getEmail), gen -> gen.text().pattern("#a#a#a#a#a#<a href=\\"mailto:example@domain.com\\">[email protected]</a>"))
.create();
Usando ignore()
Podemos usar el método ignore()
si no queremos que ciertos campos o clases sean poblados. Por ejemplo, si queremos probar la persistencia de una instancia de Student
en la base de datos, querríamos generar un objeto con un id
nulo.
Student student = Instancio.of(Student.class)
.ignore(field(Student::getId))
.create();
Usando withNullable()
Si bien Instancio es propenso a generar objetos completamente poblados, a veces esto no es deseable. Por ejemplo, es posible que queramos verificar si nuestro código funciona correctamente cuando algunos campos opcionales son nulos. Podemos lograr esto utilizando el método withNullable()
.
Student student = Instancio.of(Student.class)
.withNullable(field(Student::getEmergencyContact))
.withNullable(field(ContactInfo::getEmail))
.create();
Usando subtype()
El método subtype()
nos permite especificar una implementación para un tipo abstracto o una subclase para un tipo concreto. Veamos el siguiente ejemplo, donde la clase ContactInfo
declara un campo List<Phone>
:
Student student = Instancio.of(Student.class)
.subtype(field(ContactInfo::getPhones), LinkedList.class)
.create();
Sin especificar el tipo de lista explícitamente, Instancio usaría ArrayList
como la implementación por defecto de List
. Podemos anular este comportamiento especificando el subtipo.
Uso de Modelos
Un modelo de Instancio es una plantilla de objeto expresada a través de la API. Los objetos creados a partir de un modelo tendrán todas las propiedades del modelo. Un modelo se puede crear llamando al método toModel()
:
Model<Student> studentModel = Instancio.of(Student.class)
.generate(field(Student::getDateOfBirth), gen -> gen.temporal().localDate().past())
.generate(field(Student::getEnrollmentYear), gen -> gen.temporal().year().past())
.generate(field(ContactInfo::getEmail), gen -> gen.text().pattern("#a#a#a#a#a#<a href=\\"mailto:example@domain.com\\">[email protected]</a>"))
.generate(field(Phone::getCountryCode), gen -> gen.string().prefix("+").digits().maxLength(2))
.toModel();
Con el modelo definido, ahora podemos utilizarlo en todos nuestros métodos de prueba. Cada método de prueba puede utilizar la plantilla como base y aplicar personalizaciones según sea necesario.
Uso de la Extensión Instancio para JUnit 5
Una preocupación común sobre usar datos aleatorios es que una prueba puede fallar debido a un conjunto de datos particular que se generó. El fallo podría deberse a un error en el código de instalación o a un error en el código de producción. Sin embargo, Instancio genera datos completamente reproducibles, y usar la InstancioExtension
facilita la reproducción de pruebas fallidas.
Por ejemplo, consideremos un servicio de inscripción que arroja una excepción si un estudiante tiene al menos un curso con una calificación de F. Podemos probar esto utilizando Instancio:
@ExtendWith(InstancioExtension.class)
class ReproducingFailedTest {
EnrollmentService enrollmentService = new EnrollmentService();
@Test
void whenGivenNoFailingGrades_thenShouldEnrollStudentInCourse() {
Course course = Instancio.create(Course.class);
Student student = Instancio.create(Student.class);
boolean isEnrolled = enrollmentService.enrollStudent(student, course);
assertThat(isEnrolled).isTrue();
}
}
Si el test falla, se producirá un mensaje informando el nombre de la prueba y el valor de la semilla, por ejemplo:
*timestamp = 2023-01-24T13:50:12.436704221, Instancio = Test method 'enrollStudent' failed with seed: 1234*
Podemos reproducir la falla mediante la colocación de la anotación @Seed(1234)
en el método de prueba. Esto resultará en que se produzca nuevamente el mismo conjunto de datos:
@Seed(1234)
@Test
void whenGivenNoFailingGrades_thenShouldEnrollStudentInCourse() {
// test code unchanged
}
En este ejemplo, el fallo fue causado por un error en la configuración de los datos. Por lo tanto, simplemente podemos excluir la calificación F de ser generada para corregir nuestra prueba:
Student student = Instancio.of(Student.class)
.generate(all(Grade.class), gen -> gen.enumOf(Grade.class).excluding(Grade.F))
.create();
Conclusiones Prácticas
En este artículo, analizamos cómo eliminar la configuración manual de datos en las pruebas generando datos automáticamente con Instancio. También observamos cómo los modelos podrían ser utilizados para crear objetos personalizados para métodos de prueba individuales sin código repetitivo. Finalmente, revisamos cómo la extensión Instancio
para JUnit 5 ayuda a reproducir pruebas fallidas.
Para más detalles, consulte la Guía del Usuario de Instancio y el sitio del proyecto en GitHub.
Empezar a usar Instancio no solo te permitirá simplificar tus pruebas, sino que también mejorará la legibilidad y mantenibilidad de tu código de prueba. Al integrar esta herramienta, estarás mejor equipado para manejar la complejidad y asegurar la calidad de tus aplicaciones JAVA.