Una guía para pruebas unitarias y de integración robustas con JUnit

Publicado: 2022-03-11

Las pruebas de software automatizadas son de vital importancia para la calidad a largo plazo, la capacidad de mantenimiento y la extensibilidad de los proyectos de software, y para Java, JUnit es el camino hacia la automatización.

Si bien la mayor parte de este artículo se centrará en escribir pruebas unitarias sólidas y utilizar stubing, simulación e inyección de dependencia, también analizaremos JUnit y las pruebas de integración.

El marco de prueba JUnit es una herramienta común, gratuita y de código abierto para probar proyectos basados ​​en Java.

Al momento de escribir este artículo, JUnit 4 es el lanzamiento principal actual, se lanzó hace más de 10 años y la última actualización fue hace más de dos años.

JUnit 5 (con los modelos de programación y extensión de Júpiter) está en desarrollo activo. Admite mejor las funciones de lenguaje introducidas en Java 8 e incluye otras funciones nuevas e interesantes. Algunos equipos pueden encontrar JUnit 5 listo para usar, mientras que otros pueden continuar usando JUnit 4 hasta que se lance oficialmente 5. Veremos ejemplos de ambos.

Ejecutando JUnit

Las pruebas JUnit se pueden ejecutar directamente en IntelliJ, pero también se pueden ejecutar en otros IDE como Eclipse, NetBeans o incluso la línea de comandos.

Las pruebas siempre deben ejecutarse en el momento de la compilación, especialmente las pruebas unitarias. Una compilación con pruebas fallidas debe considerarse fallida, independientemente de si el problema está en la producción o en el código de prueba; esto requiere disciplina por parte del equipo y la voluntad de dar la máxima prioridad a la resolución de las pruebas fallidas, pero es necesario adherirse a la espíritu de automatización.

Las pruebas JUnit también se pueden ejecutar y generar informes mediante sistemas de integración continua como Jenkins. Los proyectos que usan herramientas como Gradle, Maven o Ant tienen la ventaja adicional de poder ejecutar pruebas como parte del proceso de compilación.

Grupos de engranajes que indican compatibilidad: JUnit 4 con NetBeans en uno, JUnit 5 con Eclipse y Gradle en otro, y el último con JUnit 5 con Maven e IntelliJ IDEA.

gradle

Como ejemplo de proyecto de Gradle para JUnit 5, consulte la sección Gradle de la guía del usuario de JUnit y el repositorio junit5-samples.git. Tenga en cuenta que también puede ejecutar pruebas que utilizan la API JUnit 4 (denominada "vintage" ).

El proyecto se puede crear en IntelliJ a través de la opción de menú Archivo > Abrir… > navegue hasta el junit-gradle-consumer sub-directory > Aceptar > Abrir como proyecto > Aceptar para importar el proyecto desde Gradle.

Para Eclipse, el complemento Buildship Gradle se puede instalar desde Ayuda > Eclipse Marketplace... El proyecto se puede importar con Archivo > Importar... > Gradle > Proyecto Gradle > Siguiente > Siguiente > Navegar hasta el junit-gradle-consumer > Siguiente > Siguiente > Finalizar.

Después de configurar el proyecto Gradle en IntelliJ o Eclipse, la ejecución de la tarea de build de Gradle incluirá la ejecución de todas las pruebas JUnit con la tarea de test . Tenga en cuenta que las pruebas pueden omitirse en ejecuciones posteriores de build si no se realizaron cambios en el código.

Para JUnit 4, consulte el uso de JUnit con Gradle wiki.

Experto

Para JUnit 5, consulte la sección Maven de la guía del usuario y el repositorio junit5-samples.git para ver un ejemplo de un proyecto Maven. Esto también puede ejecutar pruebas antiguas (las que usan la API JUnit 4).

En IntelliJ, use Archivo > Abrir… > vaya a junit-maven-consumer/pom.xml > Aceptar > Abrir como proyecto. Las pruebas se pueden ejecutar desde Maven Projects > junit5-maven-consumer > Lifecycle > Test.

En Eclipse, use Archivo > Importar… > Maven > Proyectos existentes de Maven > Siguiente > Busque el junit-maven-consumer > Con pom.xml seleccionado > Finalizar.

Las pruebas se pueden ejecutar ejecutando el proyecto como compilación de Maven... > especificar el objetivo de test > Ejecutar.

Para JUnit 4, consulte JUnit en el repositorio de Maven.

Entornos de desarrollo

Además de ejecutar pruebas a través de herramientas de compilación como Gradle o Maven, muchos IDE pueden ejecutar pruebas JUnit directamente.

IDEA IntelliJ

Se requiere IntelliJ IDEA 2016.2 o posterior para las pruebas JUnit 5, mientras que las pruebas JUnit 4 deberían funcionar en versiones anteriores de IntelliJ.

A los efectos de este artículo, es posible que desee crear un nuevo proyecto en IntelliJ desde uno de mis repositorios de GitHub ( JUnit5IntelliJ.git o JUnit4IntelliJ.git), que incluyen todos los archivos en el ejemplo de la clase Person simple y usan el Bibliotecas JUnit. La prueba se puede ejecutar con Ejecutar > Ejecutar 'Todas las pruebas'. La prueba también se puede ejecutar en IntelliJ desde la clase PersonTest .

Estos repositorios se crearon con nuevos proyectos Java de IntelliJ y crearon las estructuras de directorio src/main/java/com/example y src/test/java/com/example . El directorio src/main/java se especificó como carpeta de origen, mientras que src/test/java se especificó como carpeta de origen de prueba. Después de crear la clase PersonTest con un método de prueba anotado con @Test , es posible que no se pueda compilar, en cuyo caso IntelliJ ofrece la sugerencia de agregar JUnit 4 o JUnit 5 a la ruta de clase que se puede cargar desde la distribución de IntelliJ IDEA (ver estos respuestas en Stack Overflow para más detalles). Finalmente, se agregó una configuración de ejecución JUnit para Todas las pruebas.

Consulte también las Pautas prácticas de prueba de IntelliJ.

Eclipse

Un proyecto Java vacío en Eclipse no tendrá un directorio raíz de prueba. Esto se agregó desde Propiedades del proyecto> Ruta de compilación de Java> Agregar carpeta...> Crear nueva carpeta...> especificar el nombre de la carpeta> Finalizar. El nuevo directorio se seleccionará como carpeta de origen. Haga clic en Aceptar en los dos cuadros de diálogo restantes.

Las pruebas JUnit 4 se pueden crear con Archivo > Nuevo > Caso de prueba JUnit. Seleccione "Nueva prueba JUnit 4" y la carpeta de origen recién creada para las pruebas. Especifique una "clase bajo prueba" y un "paquete", asegurándose de que el paquete coincida con la clase bajo prueba. Luego, especifique un nombre para la clase de prueba. Después de finalizar el asistente, si se le solicita, elija "Agregar biblioteca JUnit 4" a la ruta de compilación. El proyecto o la clase de prueba individual se puede ejecutar como una prueba JUnit. Véase también Escritura de Eclipse y Ejecución de pruebas JUnit.

NetBeans

NetBeans solo admite pruebas JUnit 4. Las clases de prueba se pueden crear en un proyecto NetBeans Java con Archivo > Nuevo archivo… > Pruebas unitarias > Prueba JUnit o Prueba de clase existente. De forma predeterminada, el directorio raíz de prueba se denomina test en el directorio del proyecto.

Clase de producción simple y su caso de prueba JUnit

Echemos un vistazo a un ejemplo simple de código de producción y su código de prueba de unidad correspondiente para una clase de Person muy simple. Puede descargar el código de muestra de mi proyecto github y abrirlo a través de IntelliJ.

src/main/java/com/ejemplo/Persona.java
 package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ", " + givenName; } }

La clase Person inmutable tiene un constructor y un método getDisplayName() . Queremos probar que getDisplayName() devuelve el nombre con el formato esperado. Aquí está el código de prueba para una prueba de una sola unidad (JUnit 5):

src/test/java/com/example/PersonTest.java
 package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person("Josh", "Hayden"); String displayName = person.getDisplayName(); assertEquals("Hayden, Josh", displayName); } }

PersonTest usa @Test y aserción de JUnit 5. Para JUnit 4, la clase y el método PersonTest deben ser públicos y se deben usar diferentes importaciones. Aquí está el ejemplo de JUnit 4 Gist.

Al ejecutar la clase PersonTest en IntelliJ, la prueba pasa y los indicadores de la interfaz de usuario son de color verde.

Convenciones comunes de JUnit

Denominación

Aunque no es obligatorio, usamos convenciones comunes para nombrar la clase de prueba; específicamente, comenzamos con el nombre de la clase que se está probando ( Person ) y le agregamos "Test" ( PersonTest ). Nombrar el método de prueba es similar, comenzando con el método que se está probando ( getDisplayName() ) y anteponiéndole "test" ( testGetDisplayName() ). Si bien existen muchas otras convenciones perfectamente aceptables para nombrar métodos de prueba, es importante ser coherente en todo el equipo y el proyecto.

Nombre en Producción Nombre en Pruebas
Persona Prueba de persona
getDisplayName() testDisplayName()

Paquetes

También empleamos la convención de crear la clase PersonTest del código de prueba en el mismo paquete ( com.example ) que la clase Person del código de producción. Si usáramos un paquete diferente para las pruebas, tendríamos que usar el modificador de acceso público en las clases de código de producción, los constructores y los métodos a los que hacen referencia las pruebas unitarias, incluso cuando no sea apropiado, por lo que es mejor mantenerlos en el mismo paquete. . Sin embargo, usamos directorios de origen separados ( src/main/java y src/test/java ) ya que generalmente no queremos incluir código de prueba en las compilaciones de producción publicadas.

Estructura y Anotación

La anotación @Test (JUnit 4/5) le dice a JUnit que ejecute el método testGetDisplayName() como método de prueba e informe si pasa o falla. Siempre que todas las aserciones (si las hay) pasen y no se produzcan excepciones, se considera que la prueba pasó.

Nuestro código de prueba sigue el patrón de estructura de Arrange-Act-Assert (AAA). Otros patrones comunes incluyen Given-When-Then y Setup-Exercise-Verify-Teardown (el desmontaje generalmente no se necesita explícitamente para las pruebas unitarias), pero usamos AAA en este artículo.

Echemos un vistazo a cómo nuestro ejemplo de prueba sigue AAA. La primera línea, "arrange" crea un objeto Person que será probado:

 Person person = new Person("Josh", "Hayden");

La segunda línea, el "acto", ejercita el método Person.getDisplayName() del código de producción:

 String displayName = person.getDisplayName();

La tercera línea, "afirmar", verifica que el resultado es el esperado.

 assertEquals("Hayden, Josh", displayName);

Internamente, la llamada assertEquals() utiliza el método de igualdad del objeto String "Hayden, Josh" para verificar el valor real devuelto por las coincidencias del código de producción ( displayName ). Si no coincidía, la prueba se habría marcado como fallida.

Tenga en cuenta que las pruebas suelen tener más de una línea para cada una de estas fases AAA.

Pruebas Unitarias y Código de Producción

Ahora que hemos cubierto algunas convenciones de prueba, centremos nuestra atención en hacer que el código de producción sea comprobable.

Volvemos a nuestra clase Person , donde implementé un método para devolver la edad de una persona en función de su fecha de nacimiento. Los ejemplos de código requieren Java 8 para aprovechar la nueva fecha y las API funcionales. Así es como se ve la nueva clase Person.java :

Persona.java
 // ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }

Ejecutar esta clase (en el momento de escribir este artículo) anuncia que Joey tiene 4 años. Agreguemos un método de prueba:

PruebaPersona.java
 // ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); long age = person.getAge(); assertEquals(4, age); } }

Pasa hoy, pero ¿qué pasará cuando lo ejecutemos dentro de un año? Esta prueba no es determinista y es frágil, ya que el resultado esperado depende de la fecha actual del sistema que ejecuta la prueba.

Stubbing e inyección de un proveedor de valor

Cuando se ejecuta en producción, queremos usar la fecha actual, LocalDate.now() , para calcular la edad de la persona, pero para hacer una prueba determinista incluso dentro de un año, las pruebas deben proporcionar sus propios valores de currentDate .

Esto se conoce como inyección de dependencia. No queremos que nuestro objeto Person determine la fecha actual en sí, sino que queremos pasar esta lógica como una dependencia. Las pruebas unitarias utilizarán un valor conocido, stubed, y el código de producción permitirá que el sistema proporcione el valor real en tiempo de ejecución.

Agreguemos un proveedor de LocalDate a Person.java :

Persona.java
 // ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier<LocalDate> currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }

Para facilitar la prueba del método getAge() , lo cambiamos para usar currentDateSupplier , un proveedor de LocalDate , para recuperar la fecha actual. Si no sabe qué es un proveedor, le recomiendo leer sobre las interfaces funcionales integradas de Lambda.

También agregamos una inyección de dependencia: el nuevo constructor de pruebas permite que las pruebas suministren sus propios valores de fecha actuales. El constructor original llama a este nuevo constructor, pasando una referencia de método estático de LocalDate::now , que proporciona un objeto LocalDate , por lo que nuestro método principal sigue funcionando como antes. ¿Qué pasa con nuestro método de prueba? PersonTest.java :

PruebaPersona.java
 // ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse("2013-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-17"); Person person = new Person("Joey", "Doe", dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }

La prueba ahora inyecta su propio valor de currentDate , por lo que nuestra prueba aún pasará cuando se ejecute el próximo año o durante cualquier año. Esto se conoce comúnmente como stubbing , o proporcionar un valor conocido para devolver, pero primero tuvimos que cambiar Person para permitir que se inyecte esta dependencia.

Tenga en cuenta la sintaxis lambda ( ()->currentDate ) al construir el objeto Person . Esto se trata como un proveedor de LocalDate , según lo requiera el nuevo constructor.

Simulación y stupping de un servicio web

Estamos listos para que nuestro objeto Person , cuya existencia completa ha estado en la memoria de JVM, se comunique con el mundo exterior. Queremos agregar dos métodos: el método publishAge() , que publicará la edad actual de la persona, y el método getThoseInCommon() , que devolverá los nombres de personas famosas que comparten el mismo cumpleaños o tienen la misma edad que nuestra Person . Supongamos que hay un servicio RESTful con el que podemos interactuar llamado "Cumpleaños de personas". Tenemos un cliente Java para ello que consta de una sola clase, BirthdaysClient .

com.ejemplo.cumpleaños.CumpleañosCliente
 package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println("publishing " + name + "'s age: " + age); // HTTP POST with name and age and possibly throw an exception } public Collection<String> findFamousNamesOfAge(long age) throws IOException { System.out.println("finding famous names of age " + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection<String> findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println("finding famous names born on day " + dayOfMonth + " of month " + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }

Mejoremos nuestra clase Person . Comenzamos agregando un nuevo método de prueba para el comportamiento deseado de publishAge() . ¿Por qué comenzar con la prueba, en lugar de la funcionalidad? Seguimos los principios del desarrollo basado en pruebas (también conocido como TDD), en el que primero escribimos la prueba y luego el código para que pase.

PruebaPersona.java
 // … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate); person.publishAge(); } }

En este punto, el código de prueba no se puede compilar porque no hemos creado el método publishAge() al que está llamando. Una vez que creamos un método Person.publishAge() vacío, todo pasa. Ahora estamos listos para que la prueba verifique que la edad de la persona realmente se publique en BirthdaysClient .

Agregar un objeto simulado

Como se trata de una prueba unitaria, debe ejecutarse rápido y en la memoria, por lo que la prueba construirá nuestro objeto Person con un BirthdaysClient simulado para que en realidad no realice una solicitud web. Luego, la prueba usará este objeto simulado para verificar que se llamó como se esperaba. Para hacer esto, agregaremos una dependencia en el marco Mockito (licencia MIT) para crear objetos simulados y luego crearemos un objeto BirthdaysClient simulado:

PruebaPersona.java
 // ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); // ... } }

Además, aumentamos la firma del constructor Person para tomar un objeto BirthdaysClient y cambiamos la prueba para inyectar el objeto BirthdaysClient simulado.

Agregar una expectativa simulada

A continuación, agregamos al final de nuestro testPublishAge una expectativa de que se llame a BirthdaysClient . Person.publishAge() debería llamarlo, como se muestra en nuestro nuevo PersonTest.java :

PruebaPersona.java
 // ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); } }

Nuestro BirthdaysClient mejorado con Mockito realiza un seguimiento de todas las llamadas que se han realizado a sus métodos, que es la forma en que verificamos que no se hayan realizado llamadas a BirthdaysClient con el método verifyZeroInteractions() antes de llamar a publishAge() . Aunque podría decirse que no es necesario, al hacer esto nos aseguramos de que el constructor no esté haciendo llamadas deshonestas. En la línea de verify() , especificamos cómo esperamos que se vea la llamada a BirthdaysClient .

Tenga en cuenta que, dado que la publicación RegularPersonAge tiene la excepción IOException en su firma, también la agregamos a la firma de nuestro método de prueba.

En este punto, la prueba falla:

 Wanted but not invoked: birthdaysClient.publishRegularPersonAge( "Joe Sixteen", 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)

Esto es de esperar, dado que aún no hemos implementado los cambios necesarios en Person.java , ya que estamos siguiendo el desarrollo basado en pruebas. Ahora haremos que esta prueba pase haciendo los cambios necesarios:

Persona.java
 // ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + " " + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }

Prueba de excepciones

Hicimos que el constructor del código de producción creara una instancia de un nuevo BirthdaysClient , y publishAge() ahora llama a birthdaysClient . Todas las pruebas pasan; todo es verde ¡Genial! Pero tenga en cuenta que publishAge() se está tragando la IOException. En lugar de dejar que se derrame, queremos envolverlo con nuestra PersonException en un nuevo archivo llamado PersonException.java :

PersonException.java
 package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }

Implementamos este escenario como un nuevo método de prueba en PersonTest.java :

PruebaPersona.java
 // ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); try { person.publishAge(); fail("expected exception not thrown"); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals("Failed to publish Joe Sixteen age 16", e.getMessage()); } } }

La llamada de Mockito doThrow() stubs birthdaysClient para lanzar una excepción cuando se llama al método publishRegularPersonAge() . Si no se lanza PersonException , fallamos la prueba. De lo contrario, afirmamos que la excepción se encadenó correctamente con IOException y verificamos que el mensaje de excepción es el esperado. En este momento, debido a que no hemos implementado ningún manejo en nuestro código de producción, nuestra prueba falla porque no se lanzó la excepción esperada. Esto es lo que necesitamos cambiar en Person.java para que pase la prueba:

Persona.java
 // ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException("Failed to publish " + nameToPublish + " age " + age, e); } } }

Talones: Cuándo y Aserciones

Ahora implementamos el método Person.getThoseInCommon() , haciendo que nuestra clase Person.Java vea así.

Nuestro testGetThoseInCommon() , a diferencia de testPublishAge() , no verifica que se hayan realizado llamadas particulares a los métodos birthdaysClient . En su lugar, se utiliza when las llamadas a stub devuelven valores para las llamadas a findFamousNamesOfAge() y findFamousNamesBornOn() que getThoseInCommon() deberá realizar. A continuación, afirmamos que se devuelven los tres nombres añadidos que proporcionamos.

Envolver múltiples aserciones con el assertAll() JUnit 5 permite verificar todas las aserciones como un todo, en lugar de detenerse después de la primera aserción fallida. También incluimos un mensaje con assertTrue() para identificar nombres particulares que no están incluidos. Así es como se ve nuestro método de prueba de "camino feliz" (un escenario ideal) (tenga en cuenta que este no es un conjunto sólido de pruebas por la naturaleza de ser "camino feliz", pero hablaremos de por qué más adelante.

PruebaPersona.java
 // ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList("JoeFamous Sixteen", "Another Person")); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList("Jan TwoKnown")); Set<String> thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, "Another Person"), setContains(thoseInCommon, "Jan TwoKnown"), setContains(thoseInCommon, "JoeFamous Sixteen"), ()-> assertEquals(3, thoseInCommon.size()) ); } private <T> Executable setContains(Set<T> set, T expected) { return () -> assertTrue(set.contains(expected), "Should contain " + expected); } // ... }

Mantenga limpio el código de prueba

Aunque a menudo se pasa por alto, es igualmente importante mantener el código de prueba libre de duplicaciones enconadas. El código limpio y los principios como "no te repitas" son muy importantes para mantener una base de código de alta calidad, tanto para la producción como para el código de prueba. Tenga en cuenta que el PersonTest.java más reciente tiene cierta duplicación ahora que tenemos varios métodos de prueba.

Para arreglar esto, podemos hacer un puñado de cosas:

  • Extraiga el objeto IOException en un campo final privado.

  • Extraiga la creación del objeto Person en su propio método ( createJoeSixteenJan2() , en este caso) ya que la mayoría de los objetos Person se crean con los mismos parámetros.

  • Cree un assertCauseAndMessage() para las diversas pruebas que verifican PersonExceptions lanzadas.

Los resultados del código limpio se pueden ver en esta versión del archivo PersonTest.java.

Pruebe más que el camino feliz

¿Qué debemos hacer cuando un objeto Person tiene una fecha de nacimiento posterior a la fecha actual? Los defectos en las aplicaciones a menudo se deben a entradas inesperadas o falta de previsión en casos de esquina, borde o límite. Es importante tratar de anticipar estas situaciones lo mejor que podamos, y las pruebas unitarias suelen ser un lugar apropiado para hacerlo. Al construir nuestro Person y PersonTest , incluimos algunas pruebas para las excepciones esperadas, pero de ninguna manera estaba completo. Por ejemplo, usamos LocalDate que no representa ni almacena datos de zona horaria. Sin embargo, nuestras llamadas a LocalDate.now() devuelven una LocalDate basada en la zona horaria predeterminada del sistema, que podría ser un día anterior o posterior a la del usuario de un sistema. Estos factores deben ser considerados con las pruebas apropiadas y el comportamiento implementado.

Los límites también deben ser probados. Considere un objeto Person con un método getDaysUntilBirthday() . Las pruebas deben incluir si el cumpleaños de la persona ya pasó o no en el año actual, si el cumpleaños de la persona es hoy y cómo un año bisiesto afecta la cantidad de días. Estos escenarios se pueden cubrir comprobando un día antes del cumpleaños de la persona, el día de y un día después del cumpleaños de la persona donde el próximo año es un año bisiesto. Aquí está el código de prueba pertinente:

PruebaPersona.java
 // ... class PersonTest { private final Supplier<LocalDate> currentDateSupplier = ()-> LocalDate.parse("2015-05-02"); private final LocalDate ageJustOver5 = LocalDate.parse("2010-05-01"); private final LocalDate ageExactly5 = LocalDate.parse("2010-05-02"); private final LocalDate ageAlmost5 = LocalDate.parse("2010-05-03"); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function<Person, Long> personLongFunction) { Person person = new Person("Given", "Sur", dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }

Pruebas de integración

Nos hemos centrado principalmente en pruebas unitarias, pero JUnit también se puede usar para pruebas de integración, aceptación, funcionales y de sistema. Tales pruebas a menudo requieren más código de configuración, por ejemplo, iniciar servidores, cargar bases de datos con datos conocidos, etc. Si bien a menudo podemos ejecutar miles de pruebas unitarias en segundos, los conjuntos de pruebas de integraciones grandes pueden tardar minutos o incluso horas en ejecutarse. Las pruebas de integración generalmente no deben usarse para tratar de cubrir cada permutación o ruta a través del código; las pruebas unitarias son más apropiadas para eso.

La creación de pruebas para aplicaciones web que impulsan a los navegadores web a llenar formularios, hacer clic en botones, esperar a que se cargue el contenido, etc., se realiza comúnmente con Selenium WebDriver (licencia Apache 2.0) junto con el 'Patrón de objeto de página' (consulte la wiki de SeleniumHQ github y el artículo de Martin Fowler sobre Page Objects).

JUnit es efectivo para probar API RESTful con el uso de un cliente HTTP como Apache HTTP Client o Spring Rest Template (HowToDoInJava.com proporciona un buen ejemplo).

En nuestro caso con el objeto Person , una prueba de integración podría involucrar el uso del BirthdaysClient real en lugar de uno simulado, con una configuración que especifique la URL base del servicio People Birthdays. Luego, una prueba de integración usaría una instancia de prueba de dicho servicio, verificaría que los cumpleaños se publicaron en él y crearía personas famosas en el servicio que se devolverían.

Otras características de JUnit

JUnit tiene muchas características adicionales que aún no hemos explorado en los ejemplos. Describiremos algunos y proporcionaremos referencias para otros.

Accesorios de prueba

Cabe señalar que JUnit crea una nueva instancia de la clase de prueba para ejecutar cada método @Test . JUnit también proporciona ganchos de anotación para ejecutar métodos particulares antes o después de todos o cada uno de los métodos @Test . Estos ganchos se usan a menudo para configurar o limpiar bases de datos u objetos simulados, y difieren entre JUnit 4 y 5.

JUnidad 4 Unidad 5 ¿Para un método estático?
@BeforeClass @BeforeAll
@AfterClass @AfterAll
@Before @BeforeEach No
@After @AfterEach No

En nuestro ejemplo PersonTest , elegimos configurar el objeto simulado BirthdaysClient en los propios métodos @Test , pero a veces es necesario construir estructuras simuladas más complejas que involucren varios objetos. @BeforeEach (en JUnit 5) y @Before (en JUnit 4) suelen ser apropiados para esto.

Las anotaciones @After* son más comunes con las pruebas de integración que con las pruebas unitarias, ya que la recolección de elementos no utilizados de JVM maneja la mayoría de los objetos creados para las pruebas unitarias. Las anotaciones @BeforeClass y @BeforeAll se usan más comúnmente para las pruebas de integración que necesitan realizar costosas acciones de configuración y desmontaje una vez, en lugar de para cada método de prueba.

Para JUnit 4, consulte la guía de accesorios de prueba (los conceptos generales aún se aplican a JUnit 5).

conjuntos de pruebas

A veces desea ejecutar varias pruebas relacionadas, pero no todas las pruebas. En este caso, las agrupaciones de pruebas se pueden componer en conjuntos de pruebas. Para saber cómo hacer esto en JUnit 5, consulte el artículo JUnit 5 de HowToProgram.xyz y la documentación del equipo JUnit para JUnit 4.

@Nested y @DisplayName de JUnit 5

JUnit 5 agrega la capacidad de usar clases internas anidadas no estáticas para mostrar mejor la relación entre las pruebas. Esto debería ser muy familiar para aquellos que han trabajado con descripciones anidadas en marcos de prueba como Jasmine para JavaScript. Las clases internas se anotan con @Nested para usar esto.

La anotación @DisplayName también es nueva en JUnit 5, lo que le permite describir la prueba para informar en formato de cadena, que se mostrará además del identificador del método de prueba.

Aunque @Nested y @DisplayName se pueden usar de forma independiente, juntos pueden proporcionar resultados de prueba más claros que describen el comportamiento del sistema.

Emparejadores Hamcrest

El marco Hamcrest, aunque no forma parte del código base de JUnit, ofrece una alternativa al uso de métodos de afirmación tradicionales en las pruebas, lo que permite un código de prueba más expresivo y legible. Consulte la siguiente verificación usando tanto un assertEquals tradicional como un assertThat de Hamcrest:

 //Traditional assert assertEquals("Hayden, Josh", displayName); //Hamcrest assert assertThat(displayName, equalTo("Hayden, Josh"));

Hamcrest se puede usar tanto con JUnit 4 como con 5. El tutorial de Vogella.com sobre Hamcrest es bastante completo.

Recursos adicionales

  • El artículo Pruebas unitarias, cómo escribir código comprobable y por qué es importante cubre ejemplos más específicos de escritura de código limpio y comprobable.

  • Build with Confidence: A Guide to JUnit Tests examina diferentes enfoques para las pruebas unitarias y de integración, y por qué es mejor elegir uno y apegarse a él

  • La Wiki de JUnit 4 y la Guía del usuario de JUnit 5 son siempre un excelente punto de referencia.

  • La documentación de Mockito proporciona información sobre funciones adicionales y ejemplos.

JUnit es el Camino a la Automatización

Hemos explorado muchos aspectos de las pruebas en el mundo de Java con JUnit. Hemos analizado las pruebas unitarias y de integración usando el marco JUnit para bases de código Java, integrando JUnit en entornos de desarrollo y construcción, cómo usar simulacros y stubs con proveedores y Mockito, convenciones comunes y mejores prácticas de código, qué probar y algunos de los otras excelentes características de JUnit.

Ahora es el turno del lector de crecer en la aplicación, el mantenimiento y la cosecha hábil de los beneficios de las pruebas automatizadas utilizando el marco JUnit.