Escribir plantillas para casos de prueba con JUnit 5

1. Información general

La biblioteca JUnit 5 ofrece muchas características nuevas con respecto a sus versiones anteriores. Una de esas características son las plantillas de prueba. En resumen, las plantillas de prueba son una poderosa generalización de las pruebas repetidas y parametrizadas de JUnit 5.

En este tutorial, aprenderemos cómo crear una plantilla de prueba usando JUnit 5.

2. Dependencias de Maven

Comencemos agregando las dependencias a nuestro pom.xml .

Necesitamos agregar la dependencia principal de JUnit 5 junit-jupiter-engine :

 org.junit.jupiter junit-jupiter-engine 5.7.0 

Además de esto, también necesitaremos agregar la dependencia junit-jupiter-api :

 org.junit.jupiter junit-jupiter-api 5.7.0 

Asimismo, podemos agregar las dependencias necesarias a nuestro archivo build.gradle :

testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.7.0' testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.7.0'

3. La declaración del problema

Antes de mirar las plantillas de prueba, echemos un vistazo brevemente a las pruebas parametrizadas de JUnit 5. Las pruebas parametrizadas nos permiten inyectar diferentes parámetros en el método de prueba. Como resultado, cuando usamos pruebas parametrizadas, podemos ejecutar un solo método de prueba varias veces con diferentes parámetros.

Supongamos que ahora nos gustaría ejecutar nuestro método de prueba varias veces, no solo con diferentes parámetros, sino también bajo un contexto de invocación diferente cada vez.

En otras palabras, nos gustaría que el método de prueba se ejecute varias veces, con cada invocación utilizando una combinación diferente de configuraciones como:

  • usando diferentes parámetros
  • preparar la instancia de la clase de prueba de manera diferente, es decir, inyectar diferentes dependencias en la instancia de prueba
  • ejecutar la prueba en diferentes condiciones, como habilitar / deshabilitar un subconjunto de invocaciones si el entorno es " QA "
  • ejecutándose con un comportamiento de devolución de llamada de ciclo de vida diferente; tal vez queramos configurar y eliminar una base de datos antes y después de un subconjunto de invocaciones

El uso de pruebas parametrizadas rápidamente resulta limitado en este caso. Afortunadamente, JUnit 5 ofrece una solución poderosa para este escenario en forma de plantillas de prueba.

4. Plantillas de prueba

Las plantillas de prueba en sí mismas no son casos de prueba. En cambio, como sugiere su nombre, son solo plantillas para casos de prueba determinados. Son una poderosa generalización de pruebas parametrizadas y repetidas.

Las plantillas de prueba se invocan una vez para cada contexto de invocación que les proporciona el proveedor o proveedores de contexto de invocación.

Veamos ahora un ejemplo de las plantillas de prueba. Como establecimos anteriormente, los principales actores son:

  • un método de prueba de destino
  • un método de plantilla de prueba
  • uno o más proveedores de contexto de invocación registrados con el método de plantilla
  • uno o más contextos de invocación proporcionados por cada proveedor de contexto de invocación

4.1. El método del objetivo de prueba

Para este ejemplo, usaremos un método simple UserIdGeneratorImpl.generate como nuestro objetivo de prueba.

Definamos la clase UserIdGeneratorImpl :

public class UserIdGeneratorImpl implements UserIdGenerator { private boolean isFeatureEnabled; public UserIdGeneratorImpl(boolean isFeatureEnabled) { this.isFeatureEnabled = isFeatureEnabled; } public String generate(String firstName, String lastName) { String initialAndLastName = firstName.substring(0, 1).concat(lastName); return isFeatureEnabled ? "bael".concat(initialAndLastName) : initialAndLastName; } }

La generar método, que es nuestro objetivo prueba, toma la firstName y lastName como parámetros y genera un ID de usuario. El formato de la identificación de usuario varía, dependiendo de si un interruptor de función está habilitado o no.

Veamos cómo se ve esto:

Given feature switch is disabled When firstName = "John" and lastName = "Smith" Then "JSmith" is returned Given feature switch is enabled When firstName = "John" and lastName = "Smith" Then "baelJSmith" is returned

A continuación, escribamos el método de la plantilla de prueba.

4.2. El método de la plantilla de prueba

Aquí hay una plantilla de prueba para nuestro método de destino de prueba UserIdGeneratorImpl.generate :

public class UserIdGeneratorImplUnitTest { @TestTemplate @ExtendWith(UserIdGeneratorTestInvocationContextProvider.class) public void whenUserIdRequested_thenUserIdIsReturnedInCorrectFormat(UserIdGeneratorTestCase testCase) { UserIdGenerator userIdGenerator = new UserIdGeneratorImpl(testCase.isFeatureEnabled()); String actualUserId = userIdGenerator.generate(testCase.getFirstName(), testCase.getLastName()); assertThat(actualUserId).isEqualTo(testCase.getExpectedUserId()); } }

Echemos un vistazo más de cerca al método de la plantilla de prueba.

Para empezar, creamos nuestro método de plantilla de prueba marcándolo con la anotación JUnit 5 @TestTemplate .

Después de eso, registramos un proveedor de contexto , UserIdGeneratorTestInvocationContextProvider, usando la anotación @ExtendWith . Podemos registrar varios proveedores de contexto con la plantilla de prueba. Sin embargo, a los efectos de este ejemplo, registramos un solo proveedor.

Además, el método de plantilla recibe una instancia de UserIdGeneratorTestCase como parámetro. Esta es simplemente una clase contenedora para las entradas y el resultado esperado del caso de prueba:

public class UserIdGeneratorTestCase { private boolean isFeatureEnabled; private String firstName; private String lastName; private String expectedUserId; // Standard setters and getters }

Finalmente, invocamos el método del objetivo de prueba y afirmamos que ese resultado es el esperado

Ahora es el momento de definir nuestro proveedor de contexto de invocación .

4.3. El proveedor de contexto de invocación

Necesitamos registrar al menos un TestTemplateInvocationContextProvider con nuestra plantilla de prueba. Cada TestTemplateInvocationContextProvider registrado proporciona una secuencia de instancias de TestTemplateInvocationContext .

Previously, using the @ExtendWith annotation, we registered UserIdGeneratorTestInvocationContextProvider as our invocation provider.

Let's define this class now:

public class UserIdGeneratorTestInvocationContextProvider implements TestTemplateInvocationContextProvider { //... }

Our invocation context implements the TestTemplateInvocationContextProvider interface, which has two methods:

  • supportsTestTemplate
  • provideTestTemplateInvocationContexts

Let's start by implementing the supportsTestTemplate method:

@Override public boolean supportsTestTemplate(ExtensionContext extensionContext) { return true; }

The JUnit 5 execution engine calls the supportsTestTemplate method first to validate if the provider is applicable for the given ExecutionContext. In this case, we simply return true.

Now, let's implement the provideTestTemplateInvocationContexts method:

@Override public Stream provideTestTemplateInvocationContexts( ExtensionContext extensionContext) { boolean featureDisabled = false; boolean featureEnabled = true; return Stream.of( featureDisabledContext( new UserIdGeneratorTestCase( "Given feature switch disabled When user name is John Smith Then generated userid is JSmith", featureDisabled, "John", "Smith", "JSmith")), featureEnabledContext( new UserIdGeneratorTestCase( "Given feature switch enabled When user name is John Smith Then generated userid is baelJSmith", featureEnabled, "John", "Smith", "baelJSmith")) ); }

The purpose of the provideTestTemplateInvocationContexts method is to provide a Stream of TestTemplateInvocationContext instances. For our example, it returns two instances, provided by the methods featureDisabledContext and featureEnabledContext. Consequently, our test template will run twice.

Next, let's look at the two TestTemplateInvocationContext instances returned by these methods.

4.4. The Invocation Context Instances

The invocation contexts are implementations of the TestTemplateInvocationContext interface and implement the following methods:

  • getDisplayName – provide a test display name
  • getAdditionalExtensions – return additional extensions for the invocation context

Let's define the featureDisabledContext method that returns our first invocation context instance:

private TestTemplateInvocationContext featureDisabledContext(   UserIdGeneratorTestCase userIdGeneratorTestCase) { return new TestTemplateInvocationContext() { @Override public String getDisplayName(int invocationIndex) { return userIdGeneratorTestCase.getDisplayName(); } @Override public List getAdditionalExtensions() { return asList( new GenericTypedParameterResolver(userIdGeneratorTestCase), new BeforeTestExecutionCallback() { @Override public void beforeTestExecution(ExtensionContext extensionContext) { System.out.println("BeforeTestExecutionCallback:Disabled context"); } }, new AfterTestExecutionCallback() { @Override public void afterTestExecution(ExtensionContext extensionContext) { System.out.println("AfterTestExecutionCallback:Disabled context"); } } ); } }; }

Firstly, for the invocation context returned by the featureDisabledContext method, the extensions that we register are:

  • GenericTypedParameterResolver – a parameter resolver extension
  • BeforeTestExecutionCallback – a lifecycle callback extension that runs immediately before the test execution
  • AfterTestExecutionCallback – a lifecycle callback extension that runs immediately after the test execution

However, for the second invocation context, returned by the featureEnabledContext method, let's register a different set of extensions (keeping the GenericTypedParameterResolver):

private TestTemplateInvocationContext featureEnabledContext(   UserIdGeneratorTestCase userIdGeneratorTestCase) { return new TestTemplateInvocationContext() { @Override public String getDisplayName(int invocationIndex) { return userIdGeneratorTestCase.getDisplayName(); } @Override public List getAdditionalExtensions() { return asList( new GenericTypedParameterResolver(userIdGeneratorTestCase), new DisabledOnQAEnvironmentExtension(), new BeforeEachCallback() { @Override public void beforeEach(ExtensionContext extensionContext) { System.out.println("BeforeEachCallback:Enabled context"); } }, new AfterEachCallback() { @Override public void afterEach(ExtensionContext extensionContext) { System.out.println("AfterEachCallback:Enabled context"); } } ); } }; }

For the second invocation context, the extensions that we register are:

  • GenericTypedParameterResolver – a parameter resolver extension
  • DisabledOnQAEnvironmentExtension – an execution condition to disable the test if the environment property (loaded from the application.properties file) is “qa
  • BeforeEachCallback – a lifecycle callback extension that runs before each test method execution
  • AfterEachCallback – a lifecycle callback extension that runs after each test method execution

From the above example, it is clear to see that:

  • the same test method is run under multiple invocation contexts
  • each invocation context uses its own set of extensions that differ both in number and nature from the extensions in other invocation contexts

As a result, a test method can be invoked multiple times under a completely different invocation context each time. And by registering multiple context providers, we can provide even more additional layers of invocation contexts under which to run the test.

5. Conclusion

In this article, we looked at how JUnit 5's test templates are a powerful generalization of parameterized and repeated tests.

Para empezar, analizamos algunas limitaciones de las pruebas parametrizadas. A continuación, discutimos cómo las plantillas de prueba superan las limitaciones al permitir que una prueba se ejecute en un contexto diferente para cada invocación.

Finalmente, vimos un ejemplo de cómo crear una nueva plantilla de prueba. Desglosamos el ejemplo para comprender cómo funcionan las plantillas junto con los proveedores de contexto de invocación y los contextos de invocación.

Como siempre, el código fuente de los ejemplos utilizados en este artículo está disponible en GitHub.