Skip to content

About

JUnit 6, JUnit 5 & JUnit 4 tutorial with runnable Java examples, hands-on workshops, Maven/Gradle projects, and TestNG comparison

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Repository files navigation

JUnit 6, JUnit 5 & JUnit 4 — Java Tutorials, Examples & Workshops

Java CI with Maven Java Maven Gradle JUnit JUnit Pioneer AssertJ Hamcrest Lombok License

Current branch: JUnit 6.1.3 (master). For other versions, see Branches.

A hands-on Java testing workshop with runnable examples for JUnit 6, JUnit 5, and JUnit 4.

Learn JUnit from scratch, explore advanced features, and compare JUnit with TestNG. Includes Maven and Gradle projects, step-by-step learning paths, and practical examples for articles, workshops, tech talks, and self-study.

Originally created as 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:


📋 Table of Contents


🤔 Why this repository?

JUnit is more than @Test, assertions, and lifecycle annotations.

This project covers fundamental and advanced JUnit features with practical, runnable Java examples you can clone, execute, and adapt to your own projects.

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
  • explore runnable examples that demonstrate JUnit features in practice
  • master 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 — JUnit Platform's @Suite, @BeforeSuite, @AfterSuite, and suite-like lifecycle management 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

⬆ Back to Table of Contents


👥 Who Is This For?

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

⬆ Back to Table of Contents


🌿 Branches

This repository contains examples for multiple JUnit versions, each on its own branch:

Branch JUnit version Description
master JUnit 6 (6.1.3) 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

⬆ Back to Table of Contents


🚀 Quick Start

ℹ️ Choose Your Build Tool:
This project supports both Maven and Gradle independently. Choose the tool you prefer (or use the included wrappers) and consistently use its corresponding commands:

  • Maven: use ./mvnw (Linux/macOS) or mvnw.cmd (Windows)
  • Gradle: use ./gradlew (Linux/macOS) or gradlew.bat (Windows)
git clone https://github.com/a-oleynik/junit-workshop.git
cd junit-workshop

# Maven
./mvnw clean test

# Gradle
./gradlew clean test

Import into an IDE

The same checkout can be imported as either build system:

  • Maven project: open or import pom.xml
  • Gradle project: open or import settings.gradle / build.gradle

In IntelliJ IDEA, open the repository folder and choose the build model you want when the IDE detects both Maven and Gradle. Do not link both models at the same time: select one build system for the current IDE project.

⬆ Back to Table of Contents


📦 Prerequisites

💡 Need help setting up your environment?
Check the comprehensive Java, Maven & Gradle Setup Guide (Windows, macOS, Ubuntu) covering JDK installation, JAVA_HOME configuration, build tools setup, and troubleshooting.

Tool Minimum version Notes
JDK 21 LTS Required by both build configurations
Maven 3.9+ (optional) The included Maven Wrapper can be used instead
Gradle 9.6.0 (optional) The included Gradle Wrapper can be used instead
Git 2.x+ (optional) For cloning and managing repository revisions
IDE Any IntelliJ IDEA is recommended
Lombok plugin Latest IntelliJ: Settings → Plugins → Lombok

💡 Maven and Gradle Wrappers included — no local Maven or Gradle installation is required. Each wrapper automatically downloads its pinned build-tool version on first run.

# Linux / macOS (or Windows with Git Bash)
./mvnw clean test
./gradlew clean test

# Windows (Command Prompt / PowerShell)
mvnw.cmd clean test
gradlew.bat clean test

⬆ Back to Table of Contents


📁 Project Structure

.github/
└── copilot-instructions.md  # GitHub Copilot workspace instructions (always-on)
.junie/
└── guidelines.md            # JetBrains Junie project guidelines
.mvn/
└── wrapper/
    └── maven-wrapper.properties
build/                       # Generated by Gradle — not committed to git
├── reports/tests/test/
│   └── index.html           # Gradle HTML test report
└── test-results/test/       # Raw JUnit XML results
docs/
└── junit-6-from-scratch.md   # Article learning path and chapter-to-code mapping
gradle/wrapper/              # Pinned Gradle distribution and wrapper bootstrap JAR
src/
├── main/java/com/oleynik/qa/workshop/junit/
│   ├── scenarios/
│   │   └── calculator/
│   │       ├── beforejunit/
│   │       │   └── Main.java    # Manual comparison before introducing JUnit
│   │       └── Calculator.java  # Article example: addition and integer division
│   └── topics/
│       ├── annotations/         # Custom annotation types and helpers
│       ├── dataproviders/       # ArgumentsProvider / DataProvider implementations
│       ├── extensions/          # DBResourceExtension, TestWatcherExtension, SuiteLikeLifecycleExtension
│       │   └── basic/           # LoggingExtension: first BeforeEachCallback example
│       ├── model/               # Shared domain model (MyDoubleWrapper, MyServer, ShoppingCart, User)
│       ├── Factorial.java       # Example production class for parameterized tests
│       ├── PrimeNumber.java
│       └── Utils.java           # Helper used by timeout examples
└── test/
    ├── java/com/oleynik/qa/workshop/junit/
    │   ├── scenarios/
    │   │   └── calculator/
    │   │       ├── CalculatorParameterizedTest.java
    │   │       └── CalculatorTest.java
    │   └── topics/
    │       ├── asserts/         # assertAll and AssertJ soft assertions
    │       ├── conditional/     # Assumptions (assumeTrue, assumingThat)
    │       ├── ddt/             # Parameterized and data-driven tests
    │       ├── extensions/      # Tests exercising custom extensions
    │       │   └── basic/       # LoggingExtensionTest: registration with @ExtendWith
    │       ├── fixtures/        # ImplicitFixtureTest and InlineFixtureTest: fresh cart per test
    │       ├── general/         # Basic assertions, lifecycle, exceptions, display names, timeouts
    │       ├── grouping/        # @Tag usage and custom tag annotations
    │       │   └── tags/        # Tags constants, @Smoke and @Regression
    │       ├── nested/          # @Nested test classes
    │       ├── order/           # @TestMethodOrder and @Order
    │       ├── repeat/          # @RepeatedTest
    │       ├── retry/           # @RetryingTest (Pioneer), @RepeatedIfExceptionsTest (Rerunner)
    │       └── suites/          # @Suite, @BeforeSuite, @AfterSuite
    │           └── extension/  # Suite-like lifecycle via BeforeAllCallback and root store
    └── resources/
        ├── junit-platform.properties # Shared JUnit configuration, including parallel execution
        └── numbers.csv         # CSVParameterizationTest input (number → expected factorial)
target/                      # Generated by Maven — not committed to git
├── site/
│   └── surefire-report.html # HTML test report — open in a browser after `mvn site`
└── surefire-reports/        # Raw JUnit XML results (TEST-*.xml) + console output (*.txt)
.gitignore
AGENTS.md                    # AI Agent task recipes and conventions
build.gradle                 # Gradle build — equivalent dependencies and test configuration
gradlew                      # Gradle Wrapper for Unix
gradlew.bat                  # Gradle Wrapper for Windows
LICENSE
mvnw                         # Maven Wrapper for Unix
mvnw.cmd                     # Maven Wrapper for Windows
pom.xml                      # Maven build — dependencies, Surefire config, profiles
README.md                    # This file
settings.gradle              # Gradle project name

⬆ Back to Table of Contents


🌍 Feature Map

topics contains examples of individual JUnit features; scenarios contains complete examples that combine several features and can be referenced by multiple articles. The Calculator scenario is in scenarios/calculator in both main and test sources.

The manual starting point is src/main/java/…/scenarios/calculator/beforejunit/Main.java; it uses the shared Calculator and leads into CalculatorTest.

Package / folder Feature demonstrated Test class(es)
topics/general Basic assertions (assertEquals, assertTrue, assertNull, …) AssertTest
topics/general Exception testing (assertThrows) ExceptionTest
topics/general Test lifecycle (@BeforeEach, @AfterEach, @BeforeAll, @AfterAll) LifecycleTest
topics/fixtures Fresh fixtures with In-line Setup and Implicit Setup InlineFixtureTest, ImplicitFixtureTest
topics/general Hamcrest matchers HamcrestTest
topics/general Timeouts (assertTimeout(), @Timeout) TimeoutTest
topics/general Timeout thread modes and ThreadLocal context TimeoutThreadModeTest
topics/general Disabling tests (@Disabled) DisabledTest
topics/general Display names & name generators DisplayNameTest, DisplayNameGenerationTest
topics/asserts JUnit grouped assertions (assertAll) AssertAllTest
topics/asserts AssertJ soft assertions SoftAssertionsAssertJTest, SoftAssertionsAssertJBDDTest
topics/asserts JUnit 5+ soft assert pattern SoftAssertTest
topics/conditional Assumptions (assumeTrue, assumeThat) AssumptionsTest, AssumptionsBeforeAllTest
topics/ddt Parameterized tests — @MethodSource ParameterizationTest
topics/ddt Parameterized tests — @ValueSource / @CsvSource ValueSourceTest
topics/ddt CSV file data source (src/test/resources/numbers.csv) CSVParameterizationTest
topics/ddt TNG DataProvider integration DataProviderTest
topics/ddt JUnit Pioneer Cartesian product PioneerCartesianProductTest
topics/nested @Nested test classes NestedTest
topics/grouping Tagging with @Tag and custom tag annotations TagsTest
topics/order Test execution ordering (@TestMethodOrder) ExecutionOrderWithTest
topics/extensions/basic First extension (BeforeEachCallback, @ExtendWith) LoggingExtensionTest
topics/extensions Custom Extension (@RegisterExtension) DBResourceExtensionTest
topics/extensions TestWatcher extension TestWatcherExtensionTest
topics/retry Retry with JUnit Pioneer (@RetryingTest) RetryPioneerTest
topics/retry Retry with Rerunner Jupiter RetryRerunnerTest
topics/repeat @RepeatedTest RetryRepeatedTest
topics/suites Suite lifecycle (@Suite, @BeforeSuite, @AfterSuite, @SelectClasses) BeforeAfterSuite, SuiteLifecycleFirstCase, SuiteLifecycleSecondCase
topics/suites/extension Suite-like global lifecycle via BeforeAllCallback + root ExtensionContext store SuiteExtensionFirstTest, SuiteExtensionSecondTest

⬆ Back to Table of Contents


📖 Article Companions

This repository is also used as companion code for articles and tutorials.

A beginner-friendly introduction that starts with manual checks in main() and gradually introduces test discovery, assertions, lifecycle, fixtures, parameterized tests, tags, assumptions, exceptions, parallel execution, build tools, and extensions.

➡️ Follow the article learning path

⬆ Back to Table of Contents


🌱 Beginner Learning Path

Work through these topics in order; each builds on the previous one.

1. Basic assertions

AssertTest
Learn assertEquals, assertTrue, assertNull, and fail.

2. Test lifecycle

LifecycleTest Understand @BeforeEach, @AfterEach, @BeforeAll, @AfterAll.

3. Fixtures (in-line vs. implicit setup)

InlineFixtureTest, ImplicitFixtureTest
Learn the difference between fixture (the test state/data) and lifecycle methods (the setup mechanism). Compare creating an empty ShoppingCart directly in each test versus using @BeforeEach.

The empty cart and its state form the fixture; creating it is fixture setup. Both examples use a Fresh Fixture: each test gets a new cart. In-line/Implicit Setup describes where preparation happens; Fresh/Shared Fixture describes whether fixture state is recreated or reused between tests. Prices and quantities stay in each test so its expected total is easy to understand. These in-memory objects need no explicit teardown; external resources may need cleanup.

4. Exception testing

ExceptionTest
Use assertThrows to assert that code throws the right exception.

5. Disabling & display names

DisabledTest, DisplayNameTest, DisplayNameGenerationTest
Skip tests cleanly and make reports human-readable.
DisplayNameGenerationTest demonstrates class-level name generators with @DisplayNameGeneration.

6. Grouped assertions

AssertAllTest
Use assertAll so multiple failures are reported together.

7. Assumptions

AssumptionsTest
Skip tests dynamically when preconditions aren't met.

8. Parameterized tests

ValueSourceTest, ParameterizationTest
Drive one test method with many data rows.

Use @ParameterizedTest(name = "...") to give each invocation a readable name in IDE results and test reports. {0}, {1}, and {2} refer to source arguments by zero-based position; {index} is the invocation number, starting at 1. For example, CalculatorParameterizedTest uses name = "{0} + {1} = {2}" to show 2 + 3 = 5. Factorial examples use {0}! = {1} with a source label. The expected value comes from the data source; formatting a name does not calculate or assert the result. ValueSourceTest keeps JUnit's default names: the argument values already identify each case.

JUnit 6 quotes text arguments before converting them to parameter types. Numeric CSV examples set quoteTextArguments = false to display numbers without quotes. The string example keeps the default quoting so null and "" remain distinguishable. See parameterized display names.

9. Nested tests

NestedTest
Organise related scenarios using inner @Nested classes.

10. Tagging

TagsTest
Mark tests as smoke or regression and run subsets from the command line.

Run the whole beginner suite:

# Maven
./mvnw clean test

# Gradle
./gradlew clean test

11. HTML Report Generation

# Maven
./mvnw clean site

# Gradle
./gradlew clean test

Maven writes the report to target/site/surefire-report.html; Gradle writes it to build/reports/tests/test/index.html automatically as part of the standard test task.

⬆ Back to Table of Contents


🎯 Advanced Learning Path

These topics assume familiarity with JUnit 5+ basics.

1. Custom Tag Annotations

TagsTest → topics/grouping/tags/ package
Compose @Tag into reusable meta-annotations (@Smoke, @Regression).

2. CSV & External Data Providers

CSVParameterizationTest, DataProviderTest, ParameterizationTest
Load test data from CSV files and external ArgumentsProvider / @MethodSource classes.

3. Cartesian Product (JUnit Pioneer)

PioneerCartesianProductTest
Generate all combinations of parameter sets automatically.

4. AssertJ fluent and soft assertions

SoftAssertionsAssertJTest, SoftAssertionsAssertJBDDTest

Use fluent assertions and collect multiple assertion failures with soft assertions. These JUnit Jupiter examples register SoftAssertionsExtension with @ExtendWith(SoftAssertionsExtension.class); the extension reports collected failures automatically after each test. SoftAssertionsAssertJTest receives SoftAssertions as a test method parameter. The BDD example injects a BDDSoftAssertions field with @InjectSoftAssertions and uses then(...). For explicit collection with SoftAssertions.assertAll(), see SoftAssertTest.

5. Hamcrest matchers

HamcrestTest

Write expressive assertions with org.hamcrest.MatcherAssert.assertThat and matchers in ordinary org.junit.jupiter.api.Test methods.

6. Custom JUnit 5+ Extensions

First extension: LoggingExtensionTest registers LoggingExtension with @ExtendWith. Its BeforeEachCallback prints the test's display name before the test body. Run the example and observe Starting test: adds_two_numbers() followed by Running the test body.

Advanced extensions: DBResourceExtensionTest uses @RegisterExtension with BeforeTestExecutionCallback and AfterTestExecutionCallback to connect and disconnect a resource around the test method. TestWatcherExtensionTest uses TestWatcher to observe outcomes and AfterAllCallback to print a summary.

7. Test Execution Order

ExecutionOrderWithTest
Control method execution order with @TestMethodOrder and @Order.

This is a demonstration of the ordering API. Keep tests independent: do not use ordering to make one test depend on state or results produced by another. Each test should prepare its own fixture and pass when run on its own.

8. Retry Strategies

RetryPioneerTest — @RetryingTest(maxAttempts, minSuccess) via JUnit Pioneer
RetryRerunnerTest — Rerunner Jupiter integration
RetryRepeatedTest — JUnit 5+ native @RepeatedTest

9. Parallel Execution

The shared defaults are configured in src/test/resources/junit-platform.properties. JUnit reads this file from the test classpath when tests run in the IDE, Maven, or Gradle:

junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=concurrent
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.executor-service=worker_thread_pool

As in the article learning path, top-level test classes may run in parallel, while methods within each class run sequentially by default. The first three settings select that execution model. The remaining settings control the thread pool: dynamic derives parallelism from the available processors, and worker_thread_pool selects JUnit 6's experimental worker-thread executor.

The same five settings are also provided as commented alternatives in pom.xml (Surefire's configurationParameters) and build.gradle (testTask.systemProperty calls). To configure parallel execution through your build tool, uncomment its example block. Those values override matching settings in junit-platform.properties for that build tool's runs; direct IDE runs continue to use the properties file unless the IDE supplies its own overrides. Keep the build-tool examples commented when using the shared defaults. See JUnit configuration precedence.

Advanced: parallel classes and methods

To allow methods within a class to run concurrently as well, replace the execution mode settings in the properties file (or in your build-tool override) with:

junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent

This is an optional workshop configuration beyond the article's defaults. Methods in classes using @TestInstance(PER_CLASS) or a MethodOrderer still require an explicit @Execution(CONCURRENT) to run concurrently. See JUnit parallel execution.

Classes running in parallel can still share mutable resources. Use the same @ResourceLock name on tests accessing a shared resource; @Execution(SAME_THREAD) keeps a class's methods sequential but does not isolate it from other classes.

// Keep this class's methods sequential, even with the advanced defaults:
@Execution(ExecutionMode.SAME_THREAD)
class MyOrderSensitiveTest { ... }

// Coordinate tests that declare the same shared resource:
@ResourceLock("shared-db-connection")
@Test
void test_that_uses_shared_resource() { ... }

💡 See ExecutionOrderWithTest in the topics/order/ package for ordering examples used alongside parallel execution.

10. Tag-based filtering

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
./mvnw clean test -Dgroups=smoke
./gradlew clean test -DincludeTags=smoke

# Run only regression tests
./mvnw clean test -Dgroups=regression
./gradlew clean test -DincludeTags=regression

# Run both tags
./mvnw clean test -Dgroups=smoke,regression
./gradlew clean test -DincludeTags=smoke,regression

Gradle also supports -DexcludeTags=TagName; both properties may be supplied together.

Via Maven profiles or dedicated Gradle tasks:

./mvnw clean test -P smoke
./gradlew clean smokeTest

./mvnw clean test -P regression
./gradlew clean regressionTest

Each Maven profile or Gradle task activates the matching tag filter and automatically excludes @Suite classes, which have no tagged members and would otherwise fail with NoTestsDiscoveredException. Profile IDs intentionally match the @Tag values — smoke and regression — so the naming is consistent throughout the project.

11. Suite Lifecycle (@BeforeSuite / @AfterSuite)

BeforeAfterSuite → topics/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 *Test or *Tests.
Use *Case or *Scenario instead.
If they matched Surefire’s or Gradle’s default discovery patterns they would execute twice —
once directly by Surefire / Gradle and once again through the suite.

⚙️ Build config: pom.xml and build.gradle both declare the JUnit Platform Suite dependency and include *Suite classes in test discovery. In Maven that is expressed through Surefire <includes>; in Gradle it is configured on every Test task.

  1. The junit-platform-suite dependency (enables @Suite, @BeforeSuite, @AfterSuite):
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-suite</artifactId>
        <!-- version managed by junit-bom in dependencyManagement -->
    </dependency>
  2. **/*Suite.java added to Surefire <includes> so BeforeAfterSuite is automatically discovered by ./mvnw clean test; the equivalent Gradle include is **/*Suite.class.

12. Suite-like Lifecycle via Extension (BeforeAllCallback + Root Store)

SuiteLikeLifecycleExtension → topics/extensions/ package (main); tests → topics/suites/extension/
An alternative to @Suite that provides global setup/teardown without grouping tests under a suite class.

How it works:

  1. Implement BeforeAllCallback
  2. Access the root ExtensionContext — shared across the entire JVM test run
  3. computeIfAbsent ensures the factory runs only once, on the first class whose beforeAll fires
  4. Return an AutoCloseable — JUnit calls close() 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 / Gradle discovery
Opt-in mechanism Declared in @SelectClasses @ExtendWith per class

ℹ️ computeIfAbsent vs getOrComputeIfAbsent: In JUnit 6, all getOrComputeIfAbsent overloads are deprecated and replaced by identically-signatured computeIfAbsent methods — this example uses computeIfAbsent(K, Function, Class<V>). On the JUnit 5 branch, use getOrComputeIfAbsent(K, Function, Class<V>) instead (same signature, different name).

13. Timeout Thread Modes

TimeoutThreadModeTest compares the two explicit thread modes of Jupiter's @Timeout:

@Timeout(value = 2, threadMode = SAME_THREAD)
// or
@Timeout(value = 2, threadMode = SEPARATE_THREAD)

The example imports both constants from org.junit.jupiter.api.Timeout.ThreadMode. Each test method has its own two-second limit. Its @BeforeEach and @AfterEach methods are outside that limit; they can be given their own @Timeout annotations. A class-level @Timeout likewise applies to each test method, not automatically to lifecycle methods. This differs from the JUnit 4 Timeout rule example, where the limit includes setup and cleanup.

@BeforeEach records its thread and sets an ordinary ThreadLocal value. The tests assert the following behavior:

Mode Test thread Value prepared in @BeforeEach
SAME_THREAD Same as the fixture thread Available
SEPARATE_THREAD Different from the fixture thread Absent

@AfterEach verifies that it runs on the original fixture thread with the original value, then removes that value in a finally block. The separate-thread test also removes its own thread-local entry. Each test has its own instance and context, so these examples work with the workshop's parallel execution enabled.

Compare with TimeoutTest: assertTimeout() runs its block on the calling thread and checks elapsed time after completion. @Timeout with SAME_THREAD interrupts the executing thread if its limit is exceeded. SEPARATE_THREAD demonstrates the context boundary that also matters with assertTimeoutPreemptively(), for example when transaction state is stored in a ThreadLocal. The tests verify the difference without intentionally timing out. See the JUnit timeout documentation.

Run this example with your chosen build tool:

# Maven
./mvnw test -Dtest=TimeoutThreadModeTest

# Gradle
./gradlew test --tests "*TimeoutThreadModeTest"

Both tests should pass.

⬆ Back to Table of Contents


💻 Command Examples

💡 Commands below are provided in pairs for Maven and Gradle. Select the commands corresponding to your chosen build system.

Run all tests

./mvnw clean test
./gradlew clean test

Run a single test class

./mvnw clean test -Dtest=AssertTest
./gradlew clean test --tests "*AssertTest"

Run a single test method

./mvnw clean test -Dtest=AssertTest#assert_equals_multiplication_test
./gradlew clean test --tests "*AssertTest.assert_equals_multiplication_test"

Run multiple test classes

./mvnw clean test -Dtest=AssertTest,HamcrestTest
./gradlew clean test --tests "*AssertTest" --tests "*HamcrestTest"

Run test methods matching a pattern

./mvnw clean test -Dtest=AssertTest#assert_equals*
./gradlew clean test --tests "*AssertTest.assert_equals*"

Run test methods matching multiple patterns

./mvnw clean test -Dtest=AssertTest#assert_equals*+assert_boolean*
./gradlew clean test --tests "*AssertTest.assert_equals*" --tests "*AssertTest.assert_boolean*"

Rerun failing tests automatically (e.g. flaky tests)

./mvnw clean test -Dsurefire.rerunFailingTestsCount=2
./gradlew clean test -PretryCount=2

Run tests by tag

./mvnw clean test -Dgroups=regression,smoke
./gradlew clean test -DincludeTags=regression,smoke

Run tests via Maven profile or dedicated Gradle task

./mvnw clean test -P smoke
./gradlew clean smokeTest

./mvnw clean test -P regression
./gradlew clean regressionTest

Run only a specific suite

./mvnw test "-Dtest=BeforeAfterSuite"
./gradlew test --tests "*BeforeAfterSuite"

⚠️ Naming convention: suite-member classes (e.g. SuiteLifecycleFirstCase) are named *Case, not *Test or *Tests.
This prevents Surefire and Gradle from discovering them as standalone tests and running them twice —
once directly by Surefire / Gradle and once again through the suite.

Compile, test, and package

./mvnw clean package
./gradlew clean build

Compile and package without running tests

./mvnw clean package -DskipTests
./gradlew clean assemble

Generate an HTML test report

./mvnw clean surefire-report:report
./gradlew clean test

Maven report: target/reports/surefire.html — this is the standalone goal (mojo) execution, which uses its own default output directory.
Gradle report: build/reports/tests/test/index.html

Generate the full Maven site or standard Gradle test report

./mvnw clean site
./gradlew clean test

Maven report: target/site/surefire-report.html — running the Surefire Report Plugin as part of mvn site places it alongside the rest of the generated site pages, under a different path than the standalone surefire-report:report goal above.
Gradle report: build/reports/tests/test/index.html

Enable full debug logging for troubleshooting

./mvnw clean test -X
./gradlew clean test --debug

⬆ Back to Table of Contents


🧩 Supported Versions

Maven/Gradle artifact Version Purpose
junit-jupiter-engine 6.1.3 (BOM) Test engine — discovers and runs Jupiter tests; transitively provides junit-jupiter-api (all @Test, @BeforeEach, @AfterAll, … annotations)
junit-jupiter-params 6.1.3 (BOM) @ParameterizedTest, @ValueSource, @CsvSource, @MethodSource, @CsvFileSource
junit-platform-suite 6.1.3 (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.48 @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)
org.gradle.test-retry 1.6.5 Gradle plugin behind the -PretryCount=N command
Java --release 21 Java language level for compilation — enforces JDK 21 API surface (<release> is stricter than <source>+<target>)

⬆ Back to Table of Contents


🚦 CI / CD

This project uses GitHub Actions with a manually triggered workflow.

Workflow — maven.yml

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.

Jobs

Job Name Runs when Maven command Equivalent Gradle command
regression Regression — all tests Always ./mvnw -B clean site ./gradlew --no-daemon clean test
by-tag By tag — {groups} Only when groups input is filled in ./mvnw -B clean site -Dgroups={groups} ./gradlew --no-daemon clean test -DincludeTags={groups}

The current GitHub Actions workflow executes the Maven commands; the Gradle column shows the equivalent commands for local use or for adding a Gradle-based workflow later.

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

How to trigger

  1. Go to the Actions tab on GitHub
  2. Select Java CI with Maven in the left panel
  3. Click Run workflow
  4. Optionally fill in the groups field (e.g. smoke) to also run the by-tag job
  5. Click Run workflow

Downloading the reports

  1. Click the completed workflow run
  2. Scroll to Artifacts → download surefire-report (HTML) or junit-xml-results (XML)
  3. Open surefire-report.html in a browser

Generating the report locally

# All tests
./mvnw clean site
./gradlew clean test

# By tag
./mvnw clean site -Dgroups=smoke
./gradlew clean test -DincludeTags=smoke

./mvnw clean site -Dgroups=regression
./gradlew clean test -DincludeTags=regression

⬆ Back to Table of Contents


🤖 AI Assistant Support

This 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 / *Scenario for suite members, never *Test)
  • assertion style (AssertJ first, Hamcrest only in HamcrestTest)
  • extension placement (src/main/…/topics/extensions/, not test sources)
  • JUnit 6 API preferences (computeIfAbsent, not the deprecated getOrComputeIfAbsent)
  • parallel execution awareness (@Execution(SAME_THREAD) / @ResourceLock where needed)

⬆ Back to Table of Contents


📚 Additional Resources

⬆ Back to Table of Contents


🔗 Useful Links

⬆ Back to Table of Contents


⭐ Found it useful?

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.


📝 License

This project is licensed under the MIT License — see the LICENSE file for details.

⬆ Back to Table of Contents


About

JUnit 6, JUnit 5 & JUnit 4 tutorial with runnable Java examples, hands-on workshops, Maven/Gradle projects, and TestNG comparison

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages