Saltar al contenido principal

Ejecutando pruebas de Serenity BDD con JUnit 5

Esta guia cubre todo lo que necesitas saber para escribir y ejecutar pruebas de Serenity BDD usando JUnit 5 (Jupiter), sin Cucumber.

Descripcion general

JUnit 5 es la version mas reciente del popular framework de pruebas de Java. Combinado con Serenity BDD, obtienes:

  • Caracteristicas de pruebas modernas: Pruebas parametrizadas, pruebas anidadas, pruebas dinamicas y mas
  • Aserciones potentes: Bibliotecas de aserciones integradas y de terceros
  • Reportes detallados: Los reportes detallados de Serenity con capturas de pantalla y detalles de ejecucion paso a paso
  • Ejecucion paralela: Ejecuta pruebas concurrentemente para obtener retroalimentacion mas rapida
  • Organizacion flexible: Etiquetas, clases anidadas y nombres de visualizacion para una mejor organizacion de pruebas
JUnit 5 Recomendado

JUnit 4 esta obsoleto a partir de Serenity 5.0.0 y sera eliminado en Serenity 6.0.0. Todos los proyectos nuevos deben usar JUnit 5.

Prerrequisitos

  • Java 17 o superior
  • Maven o Gradle
  • Un IDE con soporte para Java (IntelliJ IDEA, Eclipse, VS Code, etc.)

Configurando las dependencias

Dependencias de Maven

Agrega lo siguiente a tu pom.xml:

<properties>
<serenity.version>5.3.1</serenity.version>
<junit.version>6.0.1</junit.version>
</properties>

<dependencies>
<!-- Integracion de Serenity con JUnit 5 -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-junit5</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>

<!-- Serenity Screenplay (opcional, pero recomendado) -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-screenplay</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>

<!-- Integracion de Serenity con WebDriver (para pruebas web) -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-screenplay-webdriver</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>

<!-- JUnit 5 (Jupiter) -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>

<!-- AssertJ (recomendado para aserciones fluidas) -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
</dependencies>

Dependencias de Gradle

Para Gradle, agrega a tu build.gradle:

dependencies {
testImplementation "net.serenity-bdd:serenity-junit5:5.3.1"
testImplementation "net.serenity-bdd:serenity-screenplay:5.3.1"
testImplementation "net.serenity-bdd:serenity-screenplay-webdriver:5.3.1"

testImplementation "org.junit.jupiter:junit-jupiter-api:6.0.1"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:6.0.1"

testImplementation "org.assertj:assertj-core:3.24.2"
}

test {
useJUnitPlatform()
}

Escribiendo tu primera prueba

Estructura basica de una prueba

Cada prueba de Serenity con JUnit 5 debe estar anotada con @ExtendWith(SerenityJUnit5Extension.class):

import net.serenitybdd.junit5.SerenityJUnit5Extension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(SerenityJUnit5Extension.class)
class WhenSearchingForProducts {

@Test
void shouldFindProductByName() {
// Tu codigo de prueba aqui
}

@Test
void shouldFilterProductsByCategory() {
// Tu codigo de prueba aqui
}
}

Ejemplo de prueba web

Aqui hay un ejemplo completo de una prueba web:

import net.serenitybdd.annotations.Managed;
import net.serenitybdd.junit5.SerenityJUnit5Extension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.openqa.selenium.WebDriver;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SerenityJUnit5Extension.class)
class WhenBrowsingProducts {

@Managed(driver = "chrome", options = "headless")
WebDriver driver;

@Test
void shouldDisplayProductDetails() {
driver.get("https://example.com/products/123");

String productTitle = driver.findElement(By.id("product-title")).getText();

assertThat(productTitle).isEqualTo("Product Name");
}
}

Usando bibliotecas de pasos

Para una mejor organizacion, usa las bibliotecas de pasos de Serenity:

import net.serenitybdd.annotations.Steps;
import net.serenitybdd.junit5.SerenityJUnit5Extension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(SerenityJUnit5Extension.class)
class WhenManagingShoppingCart {

@Steps
NavigationSteps navigation;

@Steps
ProductSteps products;

@Steps
CartSteps cart;

@Test
void shouldAddProductToCart() {
navigation.openHomePage();
products.searchFor("laptop");
products.selectFirstProduct();
cart.addToCart();

cart.shouldContain(1, "items");
}
}

Ciclo de vida de las pruebas

Ciclo de vida a nivel de metodo

Usa las anotaciones de ciclo de vida de JUnit 5:

import org.junit.jupiter.api.*;

@ExtendWith(SerenityJUnit5Extension.class)
class ProductTests {

@BeforeEach
void setUp() {
// Se ejecuta antes de cada metodo de prueba
System.out.println("Setting up test");
}

@AfterEach
void tearDown() {
// Se ejecuta despues de cada metodo de prueba
System.out.println("Cleaning up test");
}

@BeforeAll
static void setUpClass() {
// Se ejecuta una vez antes de todas las pruebas en esta clase
System.out.println("Setting up test class");
}

@AfterAll
static void tearDownClass() {
// Se ejecuta una vez despues de todas las pruebas en esta clase
System.out.println("Cleaning up test class");
}

@Test
void test1() {
// Codigo de prueba
}

@Test
void test2() {
// Codigo de prueba
}
}

Orden de ejecucion

setUpClass()
setUp()
test1()
tearDown()
setUp()
test2()
tearDown()
tearDownClass()

Nombres de visualizacion y organizacion

Nombres de visualizacion personalizados

Haz que los nombres de las pruebas sean mas legibles en los reportes:

import org.junit.jupiter.api.DisplayName;

@ExtendWith(SerenityJUnit5Extension.class)
@DisplayName("Shopping Cart Management")
class ShoppingCartTests {

@Test
@DisplayName("Should add product to empty cart")
void addToEmptyCart() {
// Codigo de prueba
}

@Test
@DisplayName("Should update quantity of existing product")
void updateQuantity() {
// Codigo de prueba
}

@Test
@DisplayName("Should remove product from cart")
void removeProduct() {
// Codigo de prueba
}
}

Organizando pruebas con @Feature y @Story

Puedes usar las anotaciones @Feature y @Story para organizar tus pruebas en una jerarquía de requisitos en los reportes de Serenity. Esto te da control explícito sobre cómo aparecen las pruebas en la Documentación Viva, independientemente de la estructura de paquetes:

import net.serenitybdd.annotations.Feature;
import net.serenitybdd.annotations.Story;

@ExtendWith(SerenityJUnit5Extension.class)
@Feature("Shopping Cart")
@Story("Add item to cart")
@DisplayName("When adding items to the shopping cart")
class WhenAddingItemsTest {

@Test
@DisplayName("Should add a single item")
void addSingleItem() { /* ... */ }

@Test
@DisplayName("Should update quantity for duplicate items")
void updateQuantityForDuplicates() { /* ... */ }
}

Cuando @Story no está presente, el valor de @DisplayName se usa como nombre de la historia en la jerarquía de requisitos. Esto significa que puedes escribir:

@Feature("Shopping Cart")
@DisplayName("Add item to cart") // Se usa como nombre de la historia
class WhenAddingItemsTest { /* ... */ }

Consulta Requisitos Basados en Anotaciones para una guía completa que cubre la jerarquía @Epic > @Feature > @Story, herencia y migración desde anotaciones basadas en clases.

Pruebas anidadas

Organiza pruebas relacionadas con @Nested:

import org.junit.jupiter.api.Nested;

@ExtendWith(SerenityJUnit5Extension.class)
@DisplayName("User Authentication")
class AuthenticationTests {

@Nested
@DisplayName("When logging in")
class Login {

@Test
@DisplayName("Should succeed with valid credentials")
void validCredentials() {
// Codigo de prueba
}

@Test
@DisplayName("Should fail with invalid password")
void invalidPassword() {
// Codigo de prueba
}

@Test
@DisplayName("Should fail with non-existent user")
void nonExistentUser() {
// Codigo de prueba
}
}

@Nested
@DisplayName("When logging out")
class Logout {

@Test
@DisplayName("Should clear session")
void clearSession() {
// Codigo de prueba
}

@Test
@DisplayName("Should redirect to login page")
void redirectToLogin() {
// Codigo de prueba
}
}
}

Clases Anidadas y Anotaciones de Requisitos

Las clases @Nested heredan las anotaciones @Epic, @Feature y @Story de su clase contenedora. Esto te permite definir la funcionalidad una vez en la clase externa y asignar historias individuales a cada clase anidada:

@ExtendWith(SerenityJUnit5Extension.class)
@Feature("User Authentication")
class AuthenticationTests {

@Nested
@Story("Login")
class WhenLoggingIn {
@Test
void shouldSucceedWithValidCredentials() { /* ... */ }
}

@Nested
@Story("Logout")
class WhenLoggingOut {
@Test
void shouldClearSession() { /* ... */ }
}
}

Esto produce la jerarquía de requisitos:

User Authentication (feature)
├── Login (story)
│ └── Should succeed with valid credentials
└── Logout (story)
└── Should clear session

Consulta Requisitos Basados en Anotaciones para los detalles completos sobre la herencia de anotaciones con clases anidadas.

Pruebas parametrizadas

Pruebas parametrizadas simples

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

@ExtendWith(SerenityJUnit5Extension.class)
class SearchTests {

@Steps
SearchSteps search;

@ParameterizedTest
@ValueSource(strings = {"laptop", "phone", "tablet"})
@DisplayName("Should find products for search term: {0}")
void shouldFindProducts(String searchTerm) {
search.searchFor(searchTerm);
search.shouldSeeResults();
}
}

Fuente CSV

import org.junit.jupiter.params.provider.CsvSource;

@ParameterizedTest
@CsvSource({
"admin, admin123, Dashboard",
"user, user123, User Home",
"guest, guest123, Guest Portal"
})
@DisplayName("Should login as {0} and see {2}")
void shouldLoginSuccessfully(String username, String password, String expectedPage) {
login.as(username, password);
navigation.shouldBeOn(expectedPage);
}

Fuente de metodo

import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;

@ParameterizedTest
@MethodSource("provideTestData")
@DisplayName("Should process order for {0}")
void shouldProcessOrder(Order order) {
checkout.process(order);
checkout.shouldShowConfirmation();
}

static Stream<Order> provideTestData() {
return Stream.of(
new Order("Product1", 2, 29.99),
new Order("Product2", 1, 49.99),
new Order("Product3", 5, 9.99)
);
}

Etiquetas y filtrado

Etiquetando pruebas

Usa @Tag para categorizar pruebas:

import org.junit.jupiter.api.Tag;

@ExtendWith(SerenityJUnit5Extension.class)
class ProductTests {

@Test
@Tag("smoke")
@Tag("fast")
void quickSanityCheck() {
// Prueba de humo rapida
}

@Test
@Tag("regression")
@Tag("slow")
void comprehensiveTest() {
// Prueba de regresion completa
}

@Test
@Tag("wip")
void workInProgress() {
// Prueba en desarrollo
}
}

Ejecutando pruebas etiquetadas

Maven:

# Ejecutar solo pruebas de humo
mvn test -Dgroups=smoke

# Ejecutar smoke O regression
mvn test -Dgroups="smoke | regression"

# Ejecutar smoke Y fast
mvn test -Dgroups="smoke & fast"

# Excluir pruebas wip
mvn test -DexcludedGroups=wip

junit-platform.properties:

junit.jupiter.includeTags=smoke
junit.jupiter.excludeTags=wip,slow

Ejecucion condicional de pruebas

Pruebas especificas por sistema operativo

import org.junit.jupiter.api.condition.*;

@Test
@EnabledOnOs(OS.WINDOWS)
void runOnlyOnWindows() {
// Prueba especifica para Windows
}

@Test
@DisabledOnOs(OS.MAC)
void dontRunOnMac() {
// Prueba deshabilitada en macOS
}

Especificas por version de Java

@Test
@EnabledOnJre(JRE.JAVA_17)
void runOnlyOnJava17() {
// Prueba especifica para Java 17
}

@Test
@EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_21)
void runOnJava17To21() {
// Versiones modernas de Java
}

Condiciones personalizadas

@Test
@EnabledIf("isProductionEnvironment")
void runOnlyInProduction() {
// Prueba solo para produccion
}

boolean isProductionEnvironment() {
return System.getProperty("env", "dev").equals("prod");
}

Aserciones

Aserciones de JUnit 5

import static org.junit.jupiter.api.Assertions.*;

@Test
void demonstrateAssertions() {
// Aserciones basicas
assertEquals(expected, actual);
assertTrue(condition);
assertFalse(condition);
assertNull(object);
assertNotNull(object);

// Aserciones de arreglos
assertArrayEquals(expectedArray, actualArray);

// Aserciones de excepciones
assertThrows(IllegalArgumentException.class, () -> {
// Codigo que deberia lanzar una excepcion
});

// Aserciones de tiempo de espera
assertTimeout(Duration.ofSeconds(2), () -> {
// Codigo que deberia completarse en 2 segundos
});

// Aserciones agrupadas
assertAll("User validation",
() -> assertEquals("John", user.getFirstName()),
() -> assertEquals("Doe", user.getLastName()),
() -> assertTrue(user.isActive())
);
}

AssertJ (Recomendado)

import static org.assertj.core.api.Assertions.*;

@Test
void demonstrateAssertJ() {
// Aserciones fluidas
assertThat(actual).isEqualTo(expected);
assertThat(list).hasSize(3)
.contains("item1", "item2")
.doesNotContain("item3");

// Aserciones de cadenas
assertThat(text).startsWith("Hello")
.endsWith("World")
.contains("Test");

// Aserciones de excepciones
assertThatThrownBy(() -> methodThatThrows())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid input");

// Aserciones suaves (continuan incluso despues de fallos)
SoftAssertions softly = new SoftAssertions();
softly.assertThat(user.getName()).isEqualTo("John");
softly.assertThat(user.getAge()).isGreaterThan(18);
softly.assertAll();
}

Ejecucion paralela

Consulta la guia dedicada de Ejecucion Paralela para una cobertura completa.

Ejemplo rapido en junit-platform.properties:

# Habilitar ejecucion paralela
junit.jupiter.execution.parallel.enabled=true

# Ejecutar clases de prueba en paralelo
junit.jupiter.execution.parallel.mode.default=concurrent

# Ejecutar metodos de prueba en la misma clase secuencialmente
junit.jupiter.execution.parallel.mode.classes.default=concurrent

# Usar estrategia dinamica
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1.0

Mejores practicas

1. Nomenclatura de pruebas

// Bueno - Describe el comportamiento
@Test
void shouldCalculateDiscountForRegularCustomer() { }

// Mejor - Aun mas descriptivo
@Test
@DisplayName("Should apply 10% discount for customers with 5+ orders")
void regularCustomerDiscount() { }

// Malo - Proposito poco claro
@Test
void test1() { }

2. Organizacion de pruebas

// Bueno - Organizado por caracteristica/comportamiento
@Nested
@DisplayName("When user is logged in")
class LoggedIn {

@Nested
@DisplayName("And viewing their profile")
class ViewingProfile {

@Test
@DisplayName("Should see personal information")
void seesPersonalInfo() { }

@Test
@DisplayName("Should be able to edit details")
void canEditDetails() { }
}
}

3. Usa bibliotecas de pasos

// Bueno - Delegando a bibliotecas de pasos
@Test
void shouldCompleteCheckout() {
cart.addProduct("laptop");
checkout.proceedToCheckout();
payment.payWithCreditCard("4111111111111111");
confirmation.shouldShowOrderNumber();
}

// Malo - Implementacion de bajo nivel en la prueba
@Test
void checkout() {
driver.findElement(By.id("add-to-cart")).click();
driver.findElement(By.id("checkout-button")).click();
// ... muchas mas lineas
}

4. Datos de prueba limpios

@ExtendWith(SerenityJUnit5Extension.class)
class OrderTests {

private String orderId;

@BeforeEach
void createTestData() {
orderId = testDataFactory.createOrder();
}

@AfterEach
void cleanupTestData() {
testDataFactory.deleteOrder(orderId);
}

@Test
void shouldProcessOrder() {
// La prueba usa orderId
}
}

5. Evita la interdependencia entre pruebas

// Malo - Las pruebas dependen del orden de ejecucion
@Test
@Order(1)
void createUser() {
userId = userService.create("John");
}

@Test
@Order(2)
void updateUser() {
userService.update(userId, "Jane"); // Depende de la prueba 1
}

// Bueno - Cada prueba es independiente
@Test
void createUser() {
String userId = userService.create("John");
assertThat(userId).isNotNull();
}

@Test
void updateUser() {
String userId = userService.create("John"); // Crea sus propios datos
userService.update(userId, "Jane");
assertThat(userService.get(userId).getName()).isEqualTo("Jane");
}

Configuracion

Maven Surefire/Failsafe

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
<include>**/*TestCase.java</include>
</includes>
<systemPropertyVariables>
<webdriver.driver>${webdriver.driver}</webdriver.driver>
</systemPropertyVariables>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Propiedades de Serenity

Crea serenity.properties en src/test/resources:

# Configuracion de WebDriver
webdriver.driver=chrome
webdriver.chrome.driver=path/to/chromedriver

# Tiempos de espera
webdriver.timeouts.implicitlywait=2000
webdriver.wait.for.timeout=10000

# Reportes
serenity.project.name=My Project
serenity.test.root=net.example

# Capturas de pantalla
serenity.take.screenshots=FOR_FAILURES
serenity.take.screenshots.for.tasks=BEFORE_AND_AFTER_EACH_STEP

Solucion de problemas

Problemas comunes

1. "No tests found" (No se encontraron pruebas)

  • Asegurate de que la clase de prueba tenga @ExtendWith(SerenityJUnit5Extension.class)
  • Verifica que los metodos de prueba tengan @Test de org.junit.jupiter.api
  • Verifica que Maven/Gradle este configurado para ejecutar pruebas de JUnit 5

2. "Multiple WebDriver instances" (Multiples instancias de WebDriver)

  • Usa @Managed WebDriver - Serenity gestiona el ciclo de vida
  • No crees instancias de WebDriver manualmente
  • Asegura una limpieza adecuada en @AfterEach si es necesario

3. "Tests fail in parallel but pass individually" (Las pruebas fallan en paralelo pero pasan individualmente)

  • Verifica si hay estado compartido entre pruebas
  • Asegura que los datos de prueba esten aislados
  • Revisa la gestion de WebDriver

Proximos pasos

Recursos adicionales