Pular para o conteúdo principal

Executando Testes Serenity BDD com JUnit 5

Este guia cobre tudo o que você precisa saber para escrever e executar testes Serenity BDD usando JUnit 5 (Jupiter), sem Cucumber.

Visão Geral

JUnit 5 é a versão mais recente do popular framework de testes Java. Combinado com Serenity BDD, você obtém:

  • Recursos Modernos de Teste: Testes parametrizados, testes aninhados, testes dinâmicos e muito mais
  • Asserções Poderosas: Bibliotecas de asserção nativas e de terceiros
  • Relatórios Detalhados: Relatórios detalhados do Serenity com capturas de tela e detalhes de execução passo a passo
  • Execução Paralela: Execute testes simultaneamente para obter feedback mais rápido
  • Organização Flexível: Tags, classes aninhadas e nomes de exibição para melhor organização dos testes
JUnit 5 Recomendado

JUnit 4 está obsoleto a partir do Serenity 5.0.0 e será removido no Serenity 6.0.0. Todos os novos projetos devem usar JUnit 5.

Pré-requisitos

  • Java 17 ou superior
  • Maven ou Gradle
  • Uma IDE com suporte a Java (IntelliJ IDEA, Eclipse, VS Code, etc.)

Configurando Dependências

Dependências Maven

Adicione o seguinte ao seu pom.xml:

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

<dependencies>
<!-- Integração Serenity JUnit 5 -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-junit5</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>

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

<!-- Integração Serenity WebDriver (para testes 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 asserções fluentes) -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
</dependencies>

Dependências Gradle

Para Gradle, adicione ao seu 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()
}

Escrevendo Seu Primeiro Teste

Estrutura Básica do Teste

Todo teste Serenity JUnit 5 deve ser anotado com @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() {
// Seu código de teste aqui
}

@Test
void shouldFilterProductsByCategory() {
// Seu código de teste aqui
}
}

Exemplo de Teste Web

Aqui está um exemplo completo de um teste 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 Passos

Para melhor organização, use as bibliotecas de passos do 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 do Teste

Ciclo de Vida em Nível de Método

Use as anotações de ciclo de vida do JUnit 5:

import org.junit.jupiter.api.*;

@ExtendWith(SerenityJUnit5Extension.class)
class ProductTests {

@BeforeEach
void setUp() {
// Executa antes de cada método de teste
System.out.println("Configurando teste");
}

@AfterEach
void tearDown() {
// Executa após cada método de teste
System.out.println("Limpando teste");
}

@BeforeAll
static void setUpClass() {
// Executa uma vez antes de todos os testes nesta classe
System.out.println("Configurando classe de teste");
}

@AfterAll
static void tearDownClass() {
// Executa uma vez após todos os testes nesta classe
System.out.println("Limpando classe de teste");
}

@Test
void test1() {
// Código do teste
}

@Test
void test2() {
// Código do teste
}
}

Ordem de Execução

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

Nomes de Exibição e Organização

Nomes de Exibição Personalizados

Torne os nomes dos testes mais legíveis nos relatórios:

import org.junit.jupiter.api.DisplayName;

@ExtendWith(SerenityJUnit5Extension.class)
@DisplayName("Gerenciamento do Carrinho de Compras")
class ShoppingCartTests {

@Test
@DisplayName("Deve adicionar produto ao carrinho vazio")
void addToEmptyCart() {
// Código do teste
}

@Test
@DisplayName("Deve atualizar quantidade do produto existente")
void updateQuantity() {
// Código do teste
}

@Test
@DisplayName("Deve remover produto do carrinho")
void removeProduct() {
// Código do teste
}
}

Organizando testes com @Feature e @Story

Você pode usar as anotações @Feature e @Story para organizar seus testes em uma hierarquia de requisitos nos relatórios do Serenity. Isso oferece controle explícito sobre como os testes aparecem na Documentação Viva, independentemente da estrutura de pacotes:

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() { /* ... */ }
}

Quando @Story não está presente, o valor de @DisplayName é usado como nome da story na hierarquia de requisitos. Isso significa que você pode escrever:

@Feature("Shopping Cart")
@DisplayName("Add item to cart") // Usado como nome da story
class WhenAddingItemsTest { /* ... */ }

Consulte Requisitos Baseados em Anotações para um guia completo cobrindo a hierarquia @Epic > @Feature > @Story, herança e migração de anotações baseadas em classes.

Testes Aninhados

Organize testes relacionados com @Nested:

import org.junit.jupiter.api.Nested;

@ExtendWith(SerenityJUnit5Extension.class)
@DisplayName("Autenticação de Usuário")
class AuthenticationTests {

@Nested
@DisplayName("Ao fazer login")
class Login {

@Test
@DisplayName("Deve ter sucesso com credenciais válidas")
void validCredentials() {
// Código do teste
}

@Test
@DisplayName("Deve falhar com senha inválida")
void invalidPassword() {
// Código do teste
}

@Test
@DisplayName("Deve falhar com usuário inexistente")
void nonExistentUser() {
// Código do teste
}
}

@Nested
@DisplayName("Ao fazer logout")
class Logout {

@Test
@DisplayName("Deve limpar a sessão")
void clearSession() {
// Código do teste
}

@Test
@DisplayName("Deve redirecionar para a página de login")
void redirectToLogin() {
// Código do teste
}
}
}

Classes Aninhadas e Anotações de Requisitos

Classes @Nested herdam as anotações @Epic, @Feature e @Story da sua classe envolvente. Isso permite definir a funcionalidade uma vez na classe externa e atribuir histórias individuais a cada classe aninhada:

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

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

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

Isso produz a hierarquia de requisitos:

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

Consulte Requisitos Baseados em Anotações para os detalhes completos sobre herança de anotações com classes aninhadas.

Testes Parametrizados

Testes Parametrizados 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("Deve encontrar produtos para o termo de busca: {0}")
void shouldFindProducts(String searchTerm) {
search.searchFor(searchTerm);
search.shouldSeeResults();
}
}

CSV Source

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

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

Method Source

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

@ParameterizedTest
@MethodSource("provideTestData")
@DisplayName("Deve processar pedido para {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)
);
}

Tags e Filtros

Marcando Testes com Tags

Use @Tag para categorizar testes:

import org.junit.jupiter.api.Tag;

@ExtendWith(SerenityJUnit5Extension.class)
class ProductTests {

@Test
@Tag("smoke")
@Tag("fast")
void quickSanityCheck() {
// Teste de fumaça rápido
}

@Test
@Tag("regression")
@Tag("slow")
void comprehensiveTest() {
// Teste de regressão completo
}

@Test
@Tag("wip")
void workInProgress() {
// Teste em desenvolvimento
}
}

Executando Testes com Tags

Maven:

# Executar apenas testes de fumaça
mvn test -Dgroups=smoke

# Executar smoke OU regression
mvn test -Dgroups="smoke | regression"

# Executar smoke E fast
mvn test -Dgroups="smoke & fast"

# Excluir testes wip
mvn test -DexcludedGroups=wip

junit-platform.properties:

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

Execução Condicional de Testes

Testes Específicos por Sistema Operacional

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

@Test
@EnabledOnOs(OS.WINDOWS)
void runOnlyOnWindows() {
// Teste específico para Windows
}

@Test
@DisabledOnOs(OS.MAC)
void dontRunOnMac() {
// Teste desabilitado no macOS
}

Específicos por Versão do Java

@Test
@EnabledOnJre(JRE.JAVA_17)
void runOnlyOnJava17() {
// Teste específico para Java 17
}

@Test
@EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_21)
void runOnJava17To21() {
// Versões modernas do Java
}

Condições Personalizadas

@Test
@EnabledIf("isProductionEnvironment")
void runOnlyInProduction() {
// Teste apenas em produção
}

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

Asserções

Asserções JUnit 5

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

@Test
void demonstrateAssertions() {
// Asserções básicas
assertEquals(expected, actual);
assertTrue(condition);
assertFalse(condition);
assertNull(object);
assertNotNull(object);

// Asserções de array
assertArrayEquals(expectedArray, actualArray);

// Asserções de exceção
assertThrows(IllegalArgumentException.class, () -> {
// Código que deve lançar exceção
});

// Asserções de timeout
assertTimeout(Duration.ofSeconds(2), () -> {
// Código que deve completar em 2 segundos
});

// Asserções agrupadas
assertAll("Validação de usuário",
() -> assertEquals("John", user.getFirstName()),
() -> assertEquals("Doe", user.getLastName()),
() -> assertTrue(user.isActive())
);
}

AssertJ (Recomendado)

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

@Test
void demonstrateAssertJ() {
// Asserções fluentes
assertThat(actual).isEqualTo(expected);
assertThat(list).hasSize(3)
.contains("item1", "item2")
.doesNotContain("item3");

// Asserções de string
assertThat(text).startsWith("Hello")
.endsWith("World")
.contains("Test");

// Asserções de exceção
assertThatThrownBy(() -> methodThatThrows())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid input");

// Soft assertions (continua mesmo após falhas)
SoftAssertions softly = new SoftAssertions();
softly.assertThat(user.getName()).isEqualTo("John");
softly.assertThat(user.getAge()).isGreaterThan(18);
softly.assertAll();
}

Execução Paralela

Veja o guia dedicado Execução Paralela para uma cobertura completa.

Exemplo rápido em junit-platform.properties:

# Habilitar execução paralela
junit.jupiter.execution.parallel.enabled=true

# Executar classes de teste em paralelo
junit.jupiter.execution.parallel.mode.default=concurrent

# Executar métodos de teste na mesma classe sequencialmente
junit.jupiter.execution.parallel.mode.classes.default=concurrent

# Usar estratégia dinâmica
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1.0

Boas Práticas

1. Nomenclatura de Testes

// Bom - Descreve o comportamento
@Test
void shouldCalculateDiscountForRegularCustomer() { }

// Melhor - Ainda mais descritivo
@Test
@DisplayName("Deve aplicar 10% de desconto para clientes com 5+ pedidos")
void regularCustomerDiscount() { }

// Ruim - Propósito não claro
@Test
void test1() { }

2. Organização de Testes

// Bom - Organizado por funcionalidade/comportamento
@Nested
@DisplayName("Quando o usuário está logado")
class LoggedIn {

@Nested
@DisplayName("E visualizando seu perfil")
class ViewingProfile {

@Test
@DisplayName("Deve ver informações pessoais")
void seesPersonalInfo() { }

@Test
@DisplayName("Deve poder editar detalhes")
void canEditDetails() { }
}
}

3. Use Bibliotecas de Passos

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

// Ruim - Implementação de baixo nível no teste
@Test
void checkout() {
driver.findElement(By.id("add-to-cart")).click();
driver.findElement(By.id("checkout-button")).click();
// ... muitas mais linhas
}

4. Dados de Teste Limpos

@ExtendWith(SerenityJUnit5Extension.class)
class OrderTests {

private String orderId;

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

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

@Test
void shouldProcessOrder() {
// Teste usa orderId
}
}

5. Evite Interdependência entre Testes

// Ruim - Testes dependem da ordem de execução
@Test
@Order(1)
void createUser() {
userId = userService.create("John");
}

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

// Bom - Cada teste é independente
@Test
void createUser() {
String userId = userService.create("John");
assertThat(userId).isNotNull();
}

@Test
void updateUser() {
String userId = userService.create("John"); // Cria seus próprios dados
userService.update(userId, "Jane");
assertThat(userService.get(userId).getName()).isEqualTo("Jane");
}

Configuração

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>

Propriedades do Serenity

Crie serenity.properties em src/test/resources:

# Configuração do WebDriver
webdriver.driver=chrome
webdriver.chrome.driver=path/to/chromedriver

# Timeouts
webdriver.timeouts.implicitlywait=2000
webdriver.wait.for.timeout=10000

# Relatórios
serenity.project.name=Meu Projeto
serenity.test.root=net.example

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

Solução de Problemas

Problemas Comuns

1. "No tests found" (Nenhum teste encontrado)

  • Certifique-se de que a classe de teste tem @ExtendWith(SerenityJUnit5Extension.class)
  • Verifique se os métodos de teste têm @Test de org.junit.jupiter.api
  • Verifique se Maven/Gradle está configurado para executar testes JUnit 5

2. "Multiple WebDriver instances" (Múltiplas instâncias de WebDriver)

  • Use @Managed WebDriver - Serenity gerencia o ciclo de vida
  • Não crie instâncias de WebDriver manualmente
  • Garanta a limpeza adequada em @AfterEach se necessário

3. "Tests fail in parallel but pass individually" (Testes falham em paralelo mas passam individualmente)

  • Verifique se há estado compartilhado entre testes
  • Garanta que os dados de teste estão isolados
  • Revise o gerenciamento do WebDriver

Próximos Passos

Recursos Adicionais