Companion code for the IT Talk "JUnit 6 vs TestNG 7".
Previous edition: TestNG vs. JUnit 4 slides · TestNG vs. JUnit 4 webinar
Related projects:
- 🧪 TestNG Workshop — companion TestNG examples to compare side-by-side with this repo
- 🌐 Selenium Example (JUnit 6 branch) — real-world Selenium WebDriver framework built on top of JUnit 6
Hands-on examples for JUnit 6, JUnit 5, and JUnit 4 in Java — covering every major feature from basic assertions to advanced extensions, parameterized tests, retry strategies, parallel execution, and suite orchestration.
Use it as a practical reference, a workshop starter kit, or a side-by-side comparison with the companion TestNG Workshop.
- Found it useful?
- Why this repository?
- Who Is This For?
- Branches
- Quick Start
- Prerequisites
- Supported Versions
- Feature Map
- Learning Path — Beginners
- Advanced Topics — Path for Senior Engineers
- Command Examples
- CI / CD
- AI Assistant Support
- Project Structure
- Additional Resources
- Useful Links
- License
If you found useful examples or information in this repository, please give it a ⭐
Your support helps the project reach more Java and QA engineers.
Most JUnit tutorials stop at @Test and @BeforeEach.
This project goes further — it covers the full JUnit 5 / JUnit 6 feature set with real, runnable code you can clone, execute, and adapt immediately.
Use this repository if you want to:
- learn JUnit from scratch with a structured, step-by-step learning path
- understand the differences between JUnit 4, JUnit 5, and JUnit 6 — what changed and why
- see real runnable examples instead of reading documentation only
- explore advanced features — extensions (
BeforeAllCallback,AfterAllCallback,TestWatcher), retry strategies, parallel execution, tagging, and parameterized tests - study automated testing patterns in Java — soft assertions, data-driven tests, conditional execution, and more
- understand suite orchestration —
@BeforeSuite,@AfterSuite,@Suite, and suite-like lifecycle via custom extensions - compare JUnit and TestNG patterns and idioms side-by-side with the companion TestNG Workshop
- use it as material for workshops, tech talks, onboarding sessions, and self-study
| Audience | What you will get |
|---|---|
| QA engineers new to JUnit 6 | A guided tour of every major feature with runnable examples |
| Java developers migrating from JUnit 4 / TestNG | Side-by-side comparison of patterns and idioms |
| Senior / lead engineers | Deep-dives into extensions, retry strategies, parallel execution, and tagging |
| Workshop facilitators | A ready-made project you can hand to attendees |
This repository contains examples for multiple JUnit versions, each on its own branch:
| Branch | JUnit version | Description |
|---|---|---|
master |
JUnit 6 (6.1.0) | Current branch — latest JUnit 6 features |
junit-5.14.4 |
JUnit 5 (5.14.4) | Stable JUnit 5 examples — the most widely used version |
junit-4.13.2 |
JUnit 4 (4.13.2) | Legacy JUnit 4 examples — useful for migration reference |
git clone https://github.com/a-oleynik/junit-workshop.git
cd junit-workshop
mvn clean test| Tool | Minimum version | Notes |
|---|---|---|
| JDK | 21 LTS | |
| Maven | 3.9+ (optional but recommended) | Not required if using the included Maven Wrapper (mvnw) |
| IDE | Any (IntelliJ IDEA recommended) | Lombok plugin required for IDE support |
| Lombok plugin | Latest | IntelliJ: Settings → Plugins → Lombok |
💡 Maven Wrapper included — this project ships with
mvnw(Linux/macOS) andmvnw.cmd(Windows).
You can use it instead of a locally installed Maven. The wrapper automatically downloads the correct Maven version on first run.# Linux / macOS ./mvnw clean test # Windows (Command Prompt / PowerShell) mvnw.cmd clean test # Windows with Git Bash ./mvnw clean test
| Maven artifact | Version | Purpose |
|---|---|---|
junit-jupiter-engine |
6.1.0 (BOM) |
Test engine — discovers and runs Jupiter tests; transitively provides junit-jupiter-api (all @Test, @BeforeEach, @AfterAll, … annotations) |
junit-jupiter-params |
6.1.0 (BOM) |
@ParameterizedTest, @ValueSource, @CsvSource, @MethodSource, @CsvFileSource |
junit-platform-suite |
6.1.0 (BOM) |
@Suite, @SelectClasses, @BeforeSuite, @AfterSuite |
junit-pioneer |
2.3.0 |
@RetryingTest, @CartesianTest, and other community extensions |
junit-jupiter-params-dataprovider |
2.12 |
TNG-style @DataProvider integration for JUnit Jupiter |
assertj-core |
3.27.7 |
Fluent assertion library; SoftAssertions for collecting multiple failures |
hamcrest-library |
3.0 |
Matcher-based assertions — assertThat(value, matcher) |
lombok |
1.18.46 |
@Builder, @Data — compile-time code generation; reduces boilerplate in model classes |
rerunner-jupiter |
2.1.6 |
@RepeatedIfExceptionsTest — auto-retry flaky tests on failure |
opencsv |
5.12.0 |
CSV file parsing for data-driven tests (CSVParameterizationTest) |
| Java source / target | 21 |
Java language level for compilation |
| Package / folder | Feature demonstrated | Test class(es) |
|---|---|---|
general |
Basic assertions (assertEquals, assertTrue, assertNull, …) |
AssertTest |
general |
Exception testing (assertThrows) |
ExceptionTest |
general |
Test fixtures (@BeforeEach, @AfterEach, @BeforeAll, @AfterAll) |
FixturesTest |
general |
Hamcrest matchers | HamcrestTest |
general |
Timeouts (@Timeout) |
TimeoutTest |
general |
Disabling tests (@Disabled) |
DisabledTest |
general |
Display names & name generators | DisplayNameTest, DisplayNameGenerationTest |
group/asserts |
Grouped / soft assertions (assertAll) |
AssertAllTest |
group/asserts |
AssertJ soft assertions | SoftAssertionsAssertJTest, SoftAssertionsAssertJBDDTest |
group/asserts |
JUnit 5+ soft assert pattern | SoftAssertTest |
conditional |
Assumptions (assumeTrue, assumeThat) |
AssumptionsTest, AssumptionsBeforeAllTest |
ddt |
Parameterized tests — @MethodSource |
ParameterizationTest |
ddt |
Parameterized tests — @ValueSource / @CsvSource |
ValueSourceTest |
ddt |
CSV file data source (src/test/resources/numbers.csv) |
CSVParameterizationTest |
ddt |
TNG DataProvider integration | DataProviderTest |
ddt |
JUnit Pioneer Cartesian product | PioneerCartesianProductTest |
nested |
@Nested test classes |
NestedTest |
grouping |
Tagging with @Tag and custom tag annotations |
TagsTest |
execution/order |
Test execution ordering (@TestMethodOrder) |
ExecutionOrderWithTest |
extensions |
Custom Extension (@RegisterExtension) |
DBResourceExtensionTest |
extensions |
TestWatcher extension |
TestWatcherExtensionTest |
retry |
Retry with JUnit Pioneer (@RetryingTest) |
RetryPioneerTest |
retry |
Retry with Rerunner Jupiter | RetryRerunnerTest |
repeat |
@RepeatedTest |
RetryRepeatedTest |
suites |
Suite lifecycle (@Suite, @BeforeSuite, @AfterSuite, @SelectClasses) |
BeforeAfterSuite, SuiteLifecycleFirstCase, SuiteLifecycleSecondCase |
suites/extension |
Suite-like global lifecycle via BeforeAllCallback + root ExtensionContext store |
SuiteExtensionFirstTest, SuiteExtensionSecondTest |
Work through these topics in order; each builds on the previous one.
-
Basic assertions →
AssertTest
LearnassertEquals,assertTrue,assertNull,assertAll, andfail. -
Test lifecycle →
FixturesTest
Understand@BeforeEach,@AfterEach,@BeforeAll,@AfterAll. -
Exception testing →
ExceptionTest
UseassertThrowsto assert that code throws the right exception. -
Disabling & display names →
DisabledTest,DisplayNameTest,DisplayNameGenerationTest
Skip tests cleanly and make reports human-readable.
DisplayNameGenerationTestdemonstrates class-level name generators with@DisplayNameGeneration. -
Hamcrest matchers →
HamcrestTest
Write expressive assertions withassertThat. -
Grouped assertions →
AssertAllTest
UseassertAllso multiple failures are reported together. -
Assumptions →
AssumptionsTest
Skip tests dynamically when preconditions aren't met. -
Parameterized tests →
ValueSourceTest,ParameterizationTest
Drive one test method with many data rows. -
Nested tests →
NestedTest
Organise related scenarios using inner@Nestedclasses. -
Tagging →
TagsTest
Mark tests asSmokeorRegressionand run subsets from the command line.
Run the whole beginner suite:
mvn clean testThese topics assume familiarity with JUnit 5+ basics.
TagsTest → tags/ package
Compose @Tag into reusable meta-annotations (@Smoke, @Regression).
CSVParameterizationTest, DataProviderTest, ParameterizationTest
Load test data from CSV files and external ArgumentsProvider / @MethodSource classes.
PioneerCartesianProductTest
Generate all combinations of parameter sets automatically.
SoftAssertionsAssertJTest, SoftAssertionsAssertJBDDTest
Collect all assertion failures before reporting — no early bail-out.
DBResourceExtensionTest, TestWatcherExtensionTest
Implement BeforeAllCallback, AfterAllCallback, and TestWatcher to manage external resources and observe test
outcomes.
ExecutionOrderWithTest
Control method execution order with @TestMethodOrder and @Order.
RetryPioneerTest — @RetryingTest(maxAttempts, minSuccess) via JUnit Pioneer
RetryRerunnerTest — Rerunner Jupiter integration
RetryRepeatedTest — JUnit 5+ native @RepeatedTest
Configured globally in pom.xml via Surefire:
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrentTests run concurrently by default. Use @ResourceLock or @Execution(SAME_THREAD) to serialise where needed.
// Serialise a single test class back to the main thread:
@Execution(ExecutionMode.SAME_THREAD)
class MyOrderSensitiveTest { ... }
// Declare a shared resource lock (prevents concurrent access):
@ResourceLock("shared-db-connection")
@Test
void test_that_uses_shared_resource() { ... }💡 See
ExecutionOrderWithTestin theexecution/order/package for ordering examples used alongside parallel execution.
Tags used in this project are Smoke and Regression (defined in Tags.java; also available as meta-annotations @Smoke and @Regression).
Direct tag filtering (works out of the box):
# Run only Smoke tests
mvn clean test -Dgroups=Smoke
# Run only Regression tests
mvn clean test -Dgroups=Regression
# Run both tags
mvn clean test -Dgroups=Smoke,RegressionVia Maven profiles (SmokeTests / RegressionTests):
mvn clean test -P SmokeTests
mvn clean test -P RegressionTests
⚠️ Note: TheSmokeTestsandRegressionTestsprofiles inpom.xmlcurrently have the<groups>line commented out. To enable profile-based tag filtering, uncomment<groups>${testcase.groups}</groups>in the Surefire plugin configuration and update the profile property values fromSmokeTests/RegressionTeststoSmoke/Regressionto match the actual@Tagvalues.
BeforeAfterSuite → suites/ package
Group multiple test classes under a single @Suite class.
Use @BeforeSuite and @AfterSuite to run setup/teardown logic that wraps the entire suite — not just a single
test class.
@Suite
@SelectClasses({
SuiteLifecycleFirstCase.class,
SuiteLifecycleSecondCase.class
})
public class BeforeAfterSuite {
@BeforeSuite
static void beforeSuite() { ... }
@AfterSuite
static void afterSuite() { ... }
}Execution lifecycle:
[BeforeSuite] @BeforeSuite
[BeforeAll] SuiteLifecycleFirstCase
first_test_in_first_case
second_test_in_first_case
[AfterAll] SuiteLifecycleFirstCase
[BeforeAll] SuiteLifecycleSecondCase
first_test_in_second_case
second_test_in_second_case
[AfterAll] SuiteLifecycleSecondCase
[AfterSuite] @AfterSuite
⚠️ Naming convention: classes selected by a suite must not be named*Testor*Tests.
Use*Caseor*Scenarioinstead.
If they matched Surefire’s default discovery patterns they would execute twice —
once directly by Surefire and once again through the suite.
⚙️ Maven config:
pom.xmlrequires two things for the suite feature to work:
- The
junit-platform-suitedependency (enables@Suite,@BeforeSuite,@AfterSuite):<dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-suite</artifactId> <!-- version managed by junit-bom in dependencyManagement --> </dependency>**/*Suite.javaadded to Surefire<includes>soBeforeAfterSuiteis automatically discovered bymvn clean test.
SuiteLikeLifecycleExtension → extensions/ package (main); tests → suites/extension/
An alternative to @Suite that provides global setup/teardown without grouping tests under a suite class.
How it works:
- Implement
BeforeAllCallback - Access the root
ExtensionContext— shared across the entire JVM test run computeIfAbsentensures the factory runs only once, on the first class whosebeforeAllfires- Return an
AutoCloseable— JUnit callsclose()when the root context tears down (end of run = “AfterSuite”)
public class SuiteLikeLifecycleExtension implements BeforeAllCallback {
@Override
public void beforeAll(ExtensionContext context) {
context.getRoot()
.getStore(NAMESPACE)
.computeIfAbsent("suite-like-resource", key -> {
System.out.println("Before entire run"); // runs only once
return new SuiteCleanupResource();
}, SuiteCleanupResource.class);
}
static class SuiteCleanupResource implements AutoCloseable {
@Override
public void close() {
System.out.println("After entire run"); // called once at end of all tests
}
}
}Apply @ExtendWith to every test class that participates in the shared lifecycle:
@ExtendWith(SuiteLikeLifecycleExtension.class)
public class SuiteExtensionFirstTest {
@Test
void first() { ...}
}Comparing the two suite-lifecycle approaches:
@Suite + @BeforeSuite |
Extension approach | |
|---|---|---|
| Test class naming | *Case / *Scenario (not *Test) |
*Test — normal, runs independently |
| Requires suite class | ✅ @SelectClasses required |
❌ No suite class needed |
| Tests run independently | ❌ Only via suite entry class | ✅ Normal Surefire discovery |
| Opt-in mechanism | Declared in @SelectClasses |
@ExtendWith per class |
ℹ️
computeIfAbsentvsgetOrComputeIfAbsent: In JUnit 6, allgetOrComputeIfAbsentoverloads are deprecated and replaced by identically-signaturedcomputeIfAbsentmethods — this example usescomputeIfAbsent(K, Function, Class<V>). On the JUnit 5 branch, usegetOrComputeIfAbsent(K, Function, Class<V>)instead (same signature, different name).
mvn clean site
# or
mvn clean surefire-report:reportmvn clean testmvn clean test -Dtest=AssertTestmvn clean test -Dtest=AssertTest#assert_equals_multiplication_testmvn clean test -Dtest=AssertTest,HamcrestTestmvn clean test -Dtest=AssertTest#assert_equals*mvn clean test -Dtest=AssertTest#assert_equals*+assert_boolean*mvn clean test -Dsurefire.rerunFailingTestsCount=2mvn clean test -Dgroups=Regression,Smokemvn test "-Dtest=BeforeAfterSuite"
⚠️ Naming convention: suite-member classes (e.g.SuiteLifecycleFirstCase) are named*Case, not*Testor*Tests.
This prevents Surefire from discovering them as standalone tests and running them twice —
once directly by Surefire and once again through the suite.
mvn clean installmvn clean install -DskipTestsmvn clean surefire-report:reportmvn clean siteReports are written to
target/site/surefire-report.html
mvn clean test -XThis project uses GitHub Actions with a manually triggered workflow.
Triggered manually from Actions → Run workflow on GitHub.
| Input | Required | Description |
|---|---|---|
groups |
No | Tag filter for the by-tag job (e.g. Smoke, Regression). Leave empty to skip that job. |
| Job | Name | Runs when | Command |
|---|---|---|---|
regression |
Regression — all tests | Always | ./mvnw -B clean site |
by-tag |
By tag — {groups} |
Only when groups input is filled in |
./mvnw -B clean site -Dgroups={groups} |
Each job uploads two artifacts after completion — including on failure (if: always()):
| Artifact | Source path | Contents |
|---|---|---|
surefire-report / surefire-report-{tag} |
target/site/ |
Full HTML report — open surefire-report.html in a browser |
junit-xml-results / junit-xml-results-{tag} |
target/surefire-reports/ |
Raw JUnit XML files — compatible with CI dashboards and report parsers |
- Go to the Actions tab on GitHub
- Select Java CI with Maven in the left panel
- Click Run workflow
- Optionally fill in the
groupsfield (e.g.Smoke) to also run the by-tag job - Click Run workflow
- Click the completed workflow run
- Scroll to Artifacts → download
surefire-report(HTML) orjunit-xml-results(XML) - Open
surefire-report.htmlin a browser
# All tests
mvn clean site
# By tag
mvn clean site -Dgroups=Smoke
mvn clean site -Dgroups=RegressionThis project ships instruction files for AI coding assistants so they automatically follow the project's conventions when generating or editing code.
| File | Tool | Purpose |
|---|---|---|
AGENTS.md |
Any AI Agent | Task recipes and conventions for AI coding agents — add a test class, extension, data provider, tag, etc. |
.github/copilot-instructions.md |
GitHub Copilot | Always-on workspace instructions — injected into every Copilot suggestion and chat turn |
.junie/guidelines.md |
JetBrains Junie | Project guidelines Junie reads before generating code |
All files encode the same critical conventions:
- naming rules (
*Case/*Scenariofor suite members, never*Test) - assertion style (AssertJ first, Hamcrest only in
HamcrestTest) - extension placement (
src/main/…/extensions/, not test sources) - JUnit 6 API preferences (
computeIfAbsent, not the deprecatedgetOrComputeIfAbsent) - parallel execution awareness (
@Execution(SAME_THREAD)/@ResourceLockwhere needed)
AGENTS.md # AI Agent task recipes and conventions
README.md # This file
LICENSE
pom.xml # Maven build — dependencies, Surefire config, profiles
mvnw / mvnw.cmd # Maven Wrapper scripts (no local Maven installation required)
.github/
└── copilot-instructions.md # GitHub Copilot workspace instructions (always-on)
.junie/
└── guidelines.md # JetBrains Junie project guidelines
src/
├── main/java/com/oleynik/qa/workshop/junit/
│ ├── annotations/ # Custom annotation types and helpers
│ ├── dataproviders/ # ArgumentsProvider / DataProvider implementations
│ ├── extensions/ # Extension implementations (SuiteLikeLifecycleExtension, DBResourceExtension, TestWatcherExtension)
│ ├── model/ # Domain model (User, MyDoubleWrapper, MyServer)
│ └── Utils.java
└── test/
├── java/com/oleynik/qa/workshop/junit/
│ ├── general/ # Core assertions, fixtures, exceptions, display names
│ ├── group/asserts/ # Grouped / soft assertions
│ ├── conditional/ # Assumptions
│ ├── ddt/ # Parameterized & data-driven tests
│ ├── nested/ # @Nested test classes
│ ├── grouping/ # @Tag / custom tag annotations
│ │ └── tags/ # @Smoke, @Regression meta-annotations and Tags constants
│ ├── execution/order/ # Test execution ordering
│ ├── extensions/ # Custom JUnit extensions
│ ├── retry/ # Retry strategies (Pioneer, Rerunner)
│ ├── repeat/ # @RepeatedTest
│ └── suites/ # Suite lifecycle (@BeforeSuite, @AfterSuite)
│ └── extension/ # Suite-like global lifecycle via BeforeAllCallback
└── resources/
└── numbers.csv # Input data for CSVParameterizationTest (number → expected factorial)
target/ # Generated by Maven — not committed to git
├── surefire-reports/ # Raw JUnit XML results (TEST-*.xml) + console output (*.txt)
└── site/
└── surefire-report.html # HTML test report — open in a browser after `mvn site`
- JUnit 6 User Guide
- JUnit Pioneer Documentation
- AssertJ Documentation
- Hamcrest Tutorial
- Lombok Features
- Maven Surefire Plugin
- Maven Surefire Report Plugin
- Maven Wrapper — run Maven without a local installation
- TestNG Workshop — companion TestNG examples
- Selenium Example — JUnit 6 branch — real-world Selenium framework using JUnit 6
- Java Download
- Maven Download
- JUnit 5 & 6 Releases
- JUnit 4 Releases
- JUnit 6 Released — Clean-Up, Modernization & Minimal Disruption
- JUnit Pioneer Releases
- Lombok Download
- IntelliJ Lombok Plugin
This project is licensed under the MIT License — see the LICENSE file for details.