Testes Orientados a Dados com Serenity BDD
Testes orientados a dados permitem que voce execute a mesma logica de teste com multiplos conjuntos de dados de entrada, tornando seus testes mais abrangentes e faceis de manter. Este guia cobre abordagens de testes orientados a dados com JUnit 5 e Cucumber.
Pre-requisitos
- Java 17 ou superior
- Serenity BDD 5.2.2 ou superior
- JUnit 5 6.0.1 ou superior (para exemplos JUnit)
- Cucumber 7.33.0 ou superior (para exemplos Cucumber)
Por que Testes Orientados a Dados?
Testes orientados a dados ajudam voce a:
- Testar multiplos cenarios com duplicacao minima de codigo
- Melhorar a cobertura de testes testando casos extremos e valores de limite
- Separar a logica de teste dos dados de teste para melhor manutencao
- Tornar os testes mais legiveis mostrando claramente o que varia entre os casos de teste
Testes Parametrizados com JUnit 5
O JUnit 5 fornece capacidades poderosas de testes parametrizados atraves de @ParameterizedTest.
Configuracao Basica
Primeiro, adicione a dependencia de parametros do JUnit Jupiter:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>6.0.1</version>
<scope>test</scope>
</dependency>
Fontes de Valores Simples
Use @ValueSource para tipos de dados simples:
import net.serenitybdd.annotations.Managed;
import net.serenitybdd.junit5.SerenityJUnit5Extension;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.openqa.selenium.WebDriver;
@ExtendWith(SerenityJUnit5Extension.class)
class SearchTests {
@Managed(driver = "chrome", options = "headless")
WebDriver driver;
@ParameterizedTest
@ValueSource(strings = {"Selenium", "Cucumber", "JUnit 5"})
void shouldFindSearchResults(String searchTerm) {
// Navegar para a pagina de busca
driver.get("https://duckduckgo.com");
// Realizar busca com cada termo
SearchPage searchPage = new SearchPage(driver);
searchPage.searchFor(searchTerm);
// Verificar resultados
assertThat(searchPage.getResults()).isNotEmpty();
}
}
Tipos suportados para @ValueSource:
strings- Valores Stringints- Valores Integerlongs- Valores Longdoubles- Valores Doublebooleans- Valores Boolean
CSV Source - Dados Inline
Use @CsvSource para multiplos parametros por caso de teste:
@ExtendWith(SerenityJUnit5Extension.class)
class LoginTests {
@Managed(driver = "chrome", options = "headless")
WebDriver driver;
@Steps
LoginActions loginActions;
@ParameterizedTest
@CsvSource({
"standard_user, secret_sauce, true",
"locked_out_user, secret_sauce, false",
"problem_user, secret_sauce, true",
"invalid_user, wrong_password, false"
})
void shouldHandleLoginScenarios(String username, String password, boolean shouldSucceed) {
loginActions.navigateToLoginPage();
loginActions.loginWith(username, password);
if (shouldSucceed) {
loginActions.shouldSeeProductsPage();
} else {
loginActions.shouldSeeErrorMessage();
}
}
}
Dicas para @CsvSource:
- Valores sao separados por virgula por padrao
- Use aspas simples para strings contendo virgulas:
'value, with, commas' - Valores vazios sao tratados como null
- Espacos em branco sao removidos por padrao
CSV Source com Delimitador Personalizado
@ParameterizedTest
@CsvSource(delimiter = '|', value = {
"Alice | alice@example.com | Premium",
"Bob | bob@example.com | Standard",
"Carol | carol@example.com | Basic"
})
void shouldHandleDifferentUserTypes(String name, String email, String accountType) {
// Implementacao do teste
}
CSV File Source - Dados Externos
Armazene dados de teste em arquivos CSV externos para melhor manutencao:
src/test/resources/test-data/users.csv:
username,password,email,expectedRole
admin,admin123,admin@example.com,ADMIN
user1,password1,user1@example.com,USER
user2,password2,user2@example.com,USER
guest,guest123,guest@example.com,GUEST
@ExtendWith(SerenityJUnit5Extension.class)
class UserRegistrationTests {
@Steps
RegistrationActions registration;
@ParameterizedTest
@CsvFileSource(resources = "/test-data/users.csv", numLinesToSkip = 1)
void shouldRegisterUsers(String username, String password, String email, String expectedRole) {
registration.registerNewUser(username, password, email);
registration.shouldHaveRole(expectedRole);
}
}
Opcoes do @CsvFileSource:
resources- Caminho para o arquivo CSV (relativo a src/test/resources)numLinesToSkip- Pular linha de cabecalho (tipicamente 1)delimiter- Delimitador personalizado (padrao e virgula)encoding- Codificacao do arquivo (padrao e UTF-8)
Method Source - Objetos Complexos
Use @MethodSource para dados de teste complexos ou quando voce precisa de mais controle:
@ExtendWith(SerenityJUnit5Extension.class)
class ProductTests {
@Steps
ShoppingActions shopping;
@ParameterizedTest
@MethodSource("productTestData")
void shouldAddProductToCart(Product product, int quantity, double expectedTotal) {
shopping.addToCart(product, quantity);
shopping.shouldShowTotal(expectedTotal);
}
static Stream<Arguments> productTestData() {
return Stream.of(
Arguments.of(new Product("Laptop", 999.99), 1, 999.99),
Arguments.of(new Product("Mouse", 29.99), 2, 59.98),
Arguments.of(new Product("Keyboard", 79.99), 3, 239.97)
);
}
}
Method Source de Classe Externa
Organize dados de teste em classes separadas:
class TestDataProviders {
static Stream<Arguments> loginScenarios() {
return Stream.of(
Arguments.of("admin", "admin123", true, "Dashboard"),
Arguments.of("user", "user123", true, "Home"),
Arguments.of("invalid", "wrong", false, "Login Error")
);
}
static Stream<Arguments> searchQueries() {
return Stream.of(
Arguments.of("Selenium", 10),
Arguments.of("Cucumber", 8),
Arguments.of("JUnit", 15)
);
}
}
@ExtendWith(SerenityJUnit5Extension.class)
class LoginTests {
@ParameterizedTest
@MethodSource("com.example.TestDataProviders#loginScenarios")
void shouldHandleLoginScenarios(String username, String password,
boolean shouldSucceed, String expectedPage) {
// Implementacao do teste
}
}
Enum Source
Teste com valores enum:
enum UserRole {
ADMIN, MANAGER, USER, GUEST
}
@ExtendWith(SerenityJUnit5Extension.class)
class RoleBasedTests {
@ParameterizedTest
@EnumSource(UserRole.class)
void shouldHandleAllUserRoles(UserRole role) {
// Testar cada role
}
@ParameterizedTest
@EnumSource(value = UserRole.class, names = {"ADMIN", "MANAGER"})
void shouldAllowAdminActions(UserRole role) {
// Testar apenas roles admin e manager
}
}
Nomes de Exibicao Personalizados
Torne os relatorios de teste mais legiveis:
@ParameterizedTest(name = "Search for ''{0}'' should return at least {1} results")
@CsvSource({
"Selenium, 10",
"Cucumber, 5",
"JUnit, 8"
})
void searchTests(String term, int minimumResults) {
// Implementacao do teste
}
Os placeholders {0}, {1} correspondem as posicoes dos parametros.
Scenario Outline do Cucumber
O Cucumber fornece Scenario Outline para testes BDD orientados a dados.
Scenario Outline Basico
features/login.feature:
Feature: User Login
Scenario Outline: Login with different credentials
Given the user is on the login page
When the user logs in with username "<username>" and password "<password>"
Then the login should <result>
And the user should see the "<page>" page
Examples:
| username | password | result | page |
| standard_user | secret_sauce | succeed | Products |
| locked_user | secret_sauce | fail | Login |
| invalid_user | wrong_pass | fail | Login |
Step Definition com Screenplay
import io.cucumber.java.en.*;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.abilities.BrowseTheWeb;
import net.serenitybdd.screenplay.actions.Open;
import static net.serenitybdd.screenplay.GivenWhenThen.*;
public class LoginStepDefinitions {
@Given("{actor} is on the login page")
public void userIsOnLoginPage(Actor actor) {
actor.attemptsTo(
Open.url("https://www.saucedemo.com")
);
}
@When("{actor} logs in with username {string} and password {string}")
public void userLogsIn(Actor actor, String username, String password) {
actor.attemptsTo(
Login.withCredentials(username, password)
);
}
@Then("the login should {word}")
public void verifyLoginResult(String result) {
// Verificacao baseada no resultado
}
@Then("{actor} should see the {string} page")
public void verifiesPage(Actor actor, String expectedPage) {
actor.should(
seeThat(TheCurrentPage.title(), containsString(expectedPage))
);
}
}
Multiplas Tabelas de Examples
Use multiplas tabelas de Examples para organizar diferentes cenarios de teste:
Feature: Shopping Cart
Scenario Outline: Add products to cart
Given the user is logged in
When the user adds "<product>" with quantity <quantity>
Then the cart total should be <total>
Examples: Single items
| product | quantity | total |
| Laptop | 1 | 999.99 |
| Mouse | 1 | 29.99 |
| Keyboard | 1 | 79.99 |
Examples: Multiple quantities
| product | quantity | total |
| Mouse | 3 | 89.97 |
| Keyboard | 2 | 159.98 |
Data Tables
Para estruturas de dados mais complexas, use Data Tables:
Feature: Bulk User Registration
Scenario: Register multiple users
Given the admin is logged in
When the admin registers the following users:
| username | email | role |
| alice | alice@example.com | Admin |
| bob | bob@example.com | User |
| charlie | charlie@example.com| Manager |
Then all users should be created successfully
Step Definition:
@When("the admin registers the following users:")
public void registerUsers(DataTable dataTable) {
List<Map<String, String>> users = dataTable.asMaps();
for (Map<String, String> user : users) {
String username = user.get("username");
String email = user.get("email");
String role = user.get("role");
actor.attemptsTo(
RegisterUser.withDetails(username, email, role)
);
}
}
Arquivos de Dados Externos com Cucumber
Carregue dados de arquivos externos:
Scenario: Register users from CSV
Given the admin is logged in
When the admin imports users from "test-data/users.csv"
Then all users should be registered
@When("the admin imports users from {string}")
public void importUsersFromFile(String filePath) {
List<User> users = UserDataLoader.fromCsv(filePath);
users.forEach(user ->
actor.attemptsTo(RegisterUser.with(user))
);
}
Fontes de Dados Externas
Lendo de Arquivos CSV
Crie um leitor de CSV reutilizavel:
public class CsvDataReader {
public static List<Map<String, String>> readCsv(String filePath) {
List<Map<String, String>> data = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(
new FileReader(filePath))) {
String headerLine = reader.readLine();
String[] headers = headerLine.split(",");
String line;
while ((line = reader.readLine()) != null) {
String[] values = line.split(",");
Map<String, String> row = new HashMap<>();
for (int i = 0; i < headers.length; i++) {
row.put(headers[i].trim(), values[i].trim());
}
data.add(row);
}
} catch (IOException e) {
throw new RuntimeException("Failed to read CSV file: " + filePath, e);
}
return data;
}
}
Lendo de Arquivos JSON
Use Jackson ou Gson para ler dados de teste JSON:
src/test/resources/test-data/products.json:
[
{
"name": "Laptop",
"price": 999.99,
"category": "Electronics",
"inStock": true
},
{
"name": "Mouse",
"price": 29.99,
"category": "Accessories",
"inStock": true
}
]
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
public class JsonDataReader {
private static final ObjectMapper mapper = new ObjectMapper();
public static <T> List<T> readJson(String filePath, Class<T> type) {
try {
InputStream stream = JsonDataReader.class
.getResourceAsStream(filePath);
return mapper.readValue(stream,
mapper.getTypeFactory().constructCollectionType(List.class, type));
} catch (IOException e) {
throw new RuntimeException("Failed to read JSON file: " + filePath, e);
}
}
}
// Uso
@MethodSource("productData")
static Stream<Product> productData() {
List<Product> products = JsonDataReader.readJson(
"/test-data/products.json",
Product.class
);
return products.stream();
}
Lendo de Arquivos Excel
Use Apache POI para arquivos Excel:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
<scope>test</scope>
</dependency>
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public class ExcelDataReader {
public static List<Map<String, String>> readExcel(String filePath, String sheetName) {
List<Map<String, String>> data = new ArrayList<>();
try (FileInputStream fis = new FileInputStream(filePath);
Workbook workbook = new XSSFWorkbook(fis)) {
Sheet sheet = workbook.getSheet(sheetName);
Row headerRow = sheet.getRow(0);
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
Map<String, String> rowData = new HashMap<>();
for (int j = 0; j < headerRow.getLastCellNum(); j++) {
String header = headerRow.getCell(j).getStringCellValue();
String value = getCellValueAsString(row.getCell(j));
rowData.put(header, value);
}
data.add(rowData);
}
} catch (IOException e) {
throw new RuntimeException("Failed to read Excel file", e);
}
return data;
}
private static String getCellValueAsString(Cell cell) {
if (cell == null) return "";
switch (cell.getCellType()) {
case STRING: return cell.getStringCellValue();
case NUMERIC: return String.valueOf(cell.getNumericCellValue());
case BOOLEAN: return String.valueOf(cell.getBooleanCellValue());
default: return "";
}
}
}
Builders de Dados de Teste
Use o padrao Builder para dados de teste complexos:
public class UserBuilder {
private String username;
private String email;
private String password;
private String role = "USER";
private boolean active = true;
public static UserBuilder aUser() {
return new UserBuilder();
}
public UserBuilder withUsername(String username) {
this.username = username;
return this;
}
public UserBuilder withEmail(String email) {
this.email = email;
return this;
}
public UserBuilder withPassword(String password) {
this.password = password;
return this;
}
public UserBuilder withRole(String role) {
this.role = role;
return this;
}
public UserBuilder inactive() {
this.active = false;
return this;
}
public User build() {
return new User(username, email, password, role, active);
}
}
// Uso
@MethodSource("userTestData")
static Stream<User> userTestData() {
return Stream.of(
aUser().withUsername("admin").withRole("ADMIN").build(),
aUser().withUsername("user1").withEmail("user1@example.com").build(),
aUser().withUsername("inactive").inactive().build()
);
}
Usando Faker para Dados Aleatorios
Gere dados de teste aleatorios realistas:
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
<scope>test</scope>
</dependency>
import com.github.javafaker.Faker;
public class TestDataGenerator {
private static final Faker faker = new Faker();
public static User randomUser() {
return aUser()
.withUsername(faker.name().username())
.withEmail(faker.internet().emailAddress())
.withPassword(faker.internet().password(8, 16))
.build();
}
public static Product randomProduct() {
return new Product(
faker.commerce().productName(),
Double.parseDouble(faker.commerce().price()),
faker.commerce().department()
);
}
}
// Uso em testes
@ParameterizedTest
@MethodSource("randomUsers")
void shouldRegisterRandomUsers(User user) {
registration.registerUser(user);
registration.shouldSucceed();
}
static Stream<User> randomUsers() {
return Stream.generate(TestDataGenerator::randomUser).limit(10);
}
Melhores Praticas
1. Mantenha os Dados de Teste Proximos aos Testes
src/test/resources/
├── test-data/
│ ├── users/
│ │ ├── valid-users.csv
│ │ └── invalid-users.csv
│ ├── products/
│ │ └── products.json
│ └── shared/
│ └── countries.csv
2. Use Nomes de Teste Significativos
@ParameterizedTest(name = "User ''{0}'' with role ''{1}'' should access ''{2}''")
@CsvSource({
"admin, ADMIN, dashboard",
"user, USER, homepage"
})
void accessControlTests(String username, String role, String page) {
// Implementacao do teste
}
3. Valide os Dados de Teste
@MethodSource("userData")
static Stream<User> userData() {
return loadUsersFromCsv("/test-data/users.csv")
.stream()
.peek(user -> {
assertThat(user.getEmail()).contains("@");
assertThat(user.getPassword()).hasSizeGreaterThan(8);
});
}
4. Isole os Dados de Teste
Garanta que cada teste tenha seus proprios dados independentes:
@BeforeEach
void setUp() {
// Limpar quaisquer dados de teste existentes
testDataCleanup.deleteTestUsers();
}
@ParameterizedTest
@MethodSource("userTestData")
void userTests(User user) {
// Cada teste recebe dados novos
String uniqueUsername = user.getUsername() + "_" + UUID.randomUUID();
User isolatedUser = user.withUsername(uniqueUsername);
// Testar com dados isolados
}
5. Documente os Requisitos de Dados
/**
* Testa o registro de usuarios com varias entradas validas.
*
* Requisitos de dados de teste:
* - Username: 3-20 caracteres, alfanumerico
* - Email: Formato de email valido
* - Password: Minimo 8 caracteres
* - Role: Um de [USER, ADMIN, MANAGER]
*/
@ParameterizedTest
@CsvFileSource(resources = "/test-data/valid-users.csv")
void shouldRegisterValidUsers(String username, String email,
String password, String role) {
// Implementacao do teste
}
6. Separe Dados Validos e Invalidos
// Testes de dados validos
@ParameterizedTest
@CsvFileSource(resources = "/test-data/valid-users.csv")
void shouldAcceptValidUsers(User user) {
// Espera sucesso
}
// Testes de dados invalidos
@ParameterizedTest
@CsvFileSource(resources = "/test-data/invalid-users.csv")
void shouldRejectInvalidUsers(User user, String expectedError) {
// Espera erro especifico
}
Exemplo Completo: Teste de E-Commerce
Aqui esta um exemplo completo combinando multiplas tecnicas:
Product.java:
public class Product {
private final String name;
private final double price;
private final String category;
private final boolean inStock;
// Construtor, getters, builder...
}
test-data/products.csv:
name,price,category,inStock,expectedInCart
Laptop,999.99,Electronics,true,true
Headphones,79.99,Audio,true,true
Out of Stock Item,49.99,Misc,false,false
ShoppingTests.java:
@ExtendWith(SerenityJUnit5Extension.class)
class ShoppingTests {
@Steps
ShoppingActions shopping;
@ParameterizedTest(name = "Adding {0} (${1}) should {4}")
@CsvFileSource(resources = "/test-data/products.csv", numLinesToSkip = 1)
void shouldHandleProductAddition(String name, double price, String category,
boolean inStock, boolean expectedInCart) {
Product product = new Product(name, price, category, inStock);
shopping.addProductToCart(product);
if (expectedInCart) {
shopping.shouldSeeProductInCart(product);
shopping.cartTotalShouldBe(price);
} else {
shopping.shouldSeeOutOfStockMessage();
}
}
}
Solucao de Problemas
Problema: Testes parametrizados nao executando
Solucao: Certifique-se de ter a dependencia junit-jupiter-params e usar @ParameterizedTest e nao @Test.
Problema: Dados CSV nao carregando
Solucao:
- Verifique se o caminho do arquivo e relativo a
src/test/resources - Verifique se
numLinesToSkipesta definido corretamente - Verifique a formatacao correta do CSV
Problema: Dados aparecendo em ordem errada
Solucao: O JUnit nao garante a ordem de execucao. Use @TestMethodOrder se necessario:
@TestMethodOrder(OrderAnnotation.class)
class OrderedTests {
@ParameterizedTest
@Order(1)
@ValueSource(strings = {"first"})
void test1(String value) { }
}
Proximos Passos
- Explore o Screenplay Pattern para melhor organizacao de testes
- Aprenda sobre Execucao Paralela para acelerar testes orientados a dados
- Confira Relatorios Serenity para ver como testes parametrizados aparecem nos relatorios