Running Serenity BDD Tests with JUnit 5
This guide covers everything you need to know to write and run Serenity BDD tests using JUnit 5 (Jupiter), without Cucumber.
Overview
JUnit 5 is the latest version of the popular Java testing framework. Combined with Serenity BDD, you get:
- Modern Test Features: Parameterized tests, nested tests, dynamic tests, and more
- Powerful Assertions: Built-in and third-party assertion libraries
- Rich Reporting: Serenity's detailed reports with screenshots and step-by-step execution details
- Parallel Execution: Run tests concurrently for faster feedback
- Flexible Organization: Tags, nested classes, and display names for better test organization
JUnit 4 is deprecated as of Serenity 5.0.0 and will be removed in Serenity 6.0.0. All new projects should use JUnit 5.
Prerequisites
- Java 17 or higher
- Maven or Gradle
- An IDE with Java support (IntelliJ IDEA, Eclipse, VS Code, etc.)
Setting Up Dependencies
Maven Dependencies
Add the following to your pom.xml:
<properties>
<serenity.version>5.0.2</serenity.version>
<junit.version>6.0.1</junit.version>
</properties>
<dependencies>
<!-- Serenity JUnit 5 Integration -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-junit5</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
<!-- Serenity Screenplay (optional, but recommended) -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-screenplay</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
<!-- Serenity WebDriver integration (for web tests) -->
<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 (recommended for fluent assertions) -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
</dependencies>
Gradle Dependencies
For Gradle, add to your build.gradle:
dependencies {
testImplementation "net.serenity-bdd:serenity-junit5:5.0.2"
testImplementation "net.serenity-bdd:serenity-screenplay:5.0.2"
testImplementation "net.serenity-bdd:serenity-screenplay-webdriver:5.0.2"
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()
}
Writing Your First Test
Basic Test Structure
Every Serenity JUnit 5 test must be annotated with @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() {
// Your test code here
}
@Test
void shouldFilterProductsByCategory() {
// Your test code here
}
}
Web Test Example
Here's a complete example of a web test:
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");
}
}
Using Step Libraries
For better organization, use Serenity step libraries:
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");
}
}
Test Lifecycle
Method-Level Lifecycle
Use JUnit 5 lifecycle annotations:
import org.junit.jupiter.api.*;
@ExtendWith(SerenityJUnit5Extension.class)
class ProductTests {
@BeforeEach
void setUp() {
// Runs before each test method
System.out.println("Setting up test");
}
@AfterEach
void tearDown() {
// Runs after each test method
System.out.println("Cleaning up test");
}
@BeforeAll
static void setUpClass() {
// Runs once before all tests in this class
System.out.println("Setting up test class");
}
@AfterAll
static void tearDownClass() {
// Runs once after all tests in this class
System.out.println("Cleaning up test class");
}
@Test
void test1() {
// Test code
}
@Test
void test2() {
// Test code
}
}
Execution Order
setUpClass()
setUp()
test1()
tearDown()
setUp()
test2()
tearDown()
tearDownClass()
Display Names and Organization
Custom Display Names
Make test names more readable in reports:
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() {
// Test code
}
@Test
@DisplayName("Should update quantity of existing product")
void updateQuantity() {
// Test code
}
@Test
@DisplayName("Should remove product from cart")
void removeProduct() {
// Test code
}
}
Nested Tests
Organize related tests with @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() {
// Test code
}
@Test
@DisplayName("Should fail with invalid password")
void invalidPassword() {
// Test code
}
@Test
@DisplayName("Should fail with non-existent user")
void nonExistentUser() {
// Test code
}
}
@Nested
@DisplayName("When logging out")
class Logout {
@Test
@DisplayName("Should clear session")
void clearSession() {
// Test code
}
@Test
@DisplayName("Should redirect to login page")
void redirectToLogin() {
// Test code
}
}
}
Parameterized Tests
Simple Parameterized Tests
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();
}
}
CSV Source
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);
}
Method Source
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)
);
}
Tags and Filtering
Tagging Tests
Use @Tag to categorize tests:
import org.junit.jupiter.api.Tag;
@ExtendWith(SerenityJUnit5Extension.class)
class ProductTests {
@Test
@Tag("smoke")
@Tag("fast")
void quickSanityCheck() {
// Fast smoke test
}
@Test
@Tag("regression")
@Tag("slow")
void comprehensiveTest() {
// Full regression test
}
@Test
@Tag("wip")
void workInProgress() {
// Test under development
}
}
Running Tagged Tests
Maven:
# Run only smoke tests
mvn test -Dgroups=smoke
# Run smoke OR regression
mvn test -Dgroups="smoke | regression"
# Run smoke AND fast
mvn test -Dgroups="smoke & fast"
# Exclude wip tests
mvn test -DexcludedGroups=wip
junit-platform.properties:
junit.jupiter.includeTags=smoke
junit.jupiter.excludeTags=wip,slow
Conditional Test Execution
OS-Specific Tests
import org.junit.jupiter.api.condition.*;
@Test
@EnabledOnOs(OS.WINDOWS)
void runOnlyOnWindows() {
// Windows-specific test
}
@Test
@DisabledOnOs(OS.MAC)
void dontRunOnMac() {
// Test disabled on macOS
}
Java Version-Specific
@Test
@EnabledOnJre(JRE.JAVA_17)
void runOnlyOnJava17() {
// Java 17 specific test
}
@Test
@EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_21)
void runOnJava17To21() {
// Modern Java versions
}
Custom Conditions
@Test
@EnabledIf("isProductionEnvironment")
void runOnlyInProduction() {
// Production-only test
}
boolean isProductionEnvironment() {
return System.getProperty("env", "dev").equals("prod");
}
Assertions
JUnit 5 Assertions
import static org.junit.jupiter.api.Assertions.*;
@Test
void demonstrateAssertions() {
// Basic assertions
assertEquals(expected, actual);
assertTrue(condition);
assertFalse(condition);
assertNull(object);
assertNotNull(object);
// Array assertions
assertArrayEquals(expectedArray, actualArray);
// Exception assertions
assertThrows(IllegalArgumentException.class, () -> {
// Code that should throw exception
});
// Timeout assertions
assertTimeout(Duration.ofSeconds(2), () -> {
// Code that should complete within 2 seconds
});
// Grouped assertions
assertAll("User validation",
() -> assertEquals("John", user.getFirstName()),
() -> assertEquals("Doe", user.getLastName()),
() -> assertTrue(user.isActive())
);
}
AssertJ (Recommended)
import static org.assertj.core.api.Assertions.*;
@Test
void demonstrateAssertJ() {
// Fluent assertions
assertThat(actual).isEqualTo(expected);
assertThat(list).hasSize(3)
.contains("item1", "item2")
.doesNotContain("item3");
// String assertions
assertThat(text).startsWith("Hello")
.endsWith("World")
.contains("Test");
// Exception assertions
assertThatThrownBy(() -> methodThatThrows())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid input");
// Soft assertions (continue even after failures)
SoftAssertions softly = new SoftAssertions();
softly.assertThat(user.getName()).isEqualTo("John");
softly.assertThat(user.getAge()).isGreaterThan(18);
softly.assertAll();
}
Parallel Execution
See the dedicated Parallel Execution Guide for comprehensive coverage.
Quick example in junit-platform.properties:
# Enable parallel execution
junit.jupiter.execution.parallel.enabled=true
# Run test classes in parallel
junit.jupiter.execution.parallel.mode.default=concurrent
# Run test methods in same class sequentially
junit.jupiter.execution.parallel.mode.classes.default=concurrent
# Use dynamic strategy
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1.0
Best Practices
1. Test Naming
// Good - Describes behavior
@Test
void shouldCalculateDiscountForRegularCustomer() { }
// Better - Even more descriptive
@Test
@DisplayName("Should apply 10% discount for customers with 5+ orders")
void regularCustomerDiscount() { }
// Bad - Unclear purpose
@Test
void test1() { }
2. Test Organization
// Good - Organized by feature/behavior
@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. Use Step Libraries
// Good - Delegating to step libraries
@Test
void shouldCompleteCheckout() {
cart.addProduct("laptop");
checkout.proceedToCheckout();
payment.payWithCreditCard("4111111111111111");
confirmation.shouldShowOrderNumber();
}
// Bad - Low-level implementation in test
@Test
void checkout() {
driver.findElement(By.id("add-to-cart")).click();
driver.findElement(By.id("checkout-button")).click();
// ... many more lines
}
4. Clean Test Data
@ExtendWith(SerenityJUnit5Extension.class)
class OrderTests {
private String orderId;
@BeforeEach
void createTestData() {
orderId = testDataFactory.createOrder();
}
@AfterEach
void cleanupTestData() {
testDataFactory.deleteOrder(orderId);
}
@Test
void shouldProcessOrder() {
// Test uses orderId
}
}
5. Avoid Test Interdependence
// Bad - Tests depend on execution order
@Test
@Order(1)
void createUser() {
userId = userService.create("John");
}
@Test
@Order(2)
void updateUser() {
userService.update(userId, "Jane"); // Depends on test 1
}
// Good - Each test is independent
@Test
void createUser() {
String userId = userService.create("John");
assertThat(userId).isNotNull();
}
@Test
void updateUser() {
String userId = userService.create("John"); // Create own data
userService.update(userId, "Jane");
assertThat(userService.get(userId).getName()).isEqualTo("Jane");
}
Configuration
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>
Serenity Properties
Create serenity.properties in src/test/resources:
# WebDriver configuration
webdriver.driver=chrome
webdriver.chrome.driver=path/to/chromedriver
# Timeouts
webdriver.timeouts.implicitlywait=2000
webdriver.wait.for.timeout=10000
# Reporting
serenity.project.name=My Project
serenity.test.root=net.example
# Screenshots
serenity.take.screenshots=FOR_FAILURES
serenity.take.screenshots.for.tasks=BEFORE_AND_AFTER_EACH_STEP
Troubleshooting
Common Issues
1. "No tests found"
- Ensure test class has
@ExtendWith(SerenityJUnit5Extension.class) - Check test methods have
@Testfromorg.junit.jupiter.api - Verify Maven/Gradle is configured to run JUnit 5 tests
2. "Multiple WebDriver instances"
- Use
@ManagedWebDriver - Serenity manages lifecycle - Don't create WebDriver instances manually
- Ensure proper cleanup in
@AfterEachif needed
3. "Tests fail in parallel but pass individually"
- Check for shared state between tests
- Ensure test data is isolated
- Review WebDriver management
Next Steps
- Explore Parallel Execution for faster test runs
- Learn about Screenplay Pattern for better test design
- Review Serenity Reports documentation
- Check out WebDriver Integration for web testing