Prev Next

Testing / JUnit6 Interview Questions

1. What is JUnit 6 and when was it released? 2. What is the JUnit 6 architecture and what are its three main components? 3. What are the core annotations in JUnit 6 and what does each do? 4. What assertions does JUnit 6 provide and how do you use them? 5. What are parameterized tests in JUnit 6 and how do you write them? 6. What is the CancellationToken API introduced in JUnit 6? 7. What are JSpecify nullability annotations in JUnit 6 and why do they matter? 8. What is the native Kotlin coroutine support in JUnit 6? 9. What are the Extension Model and extension points in JUnit 6? 10. What are nested tests in JUnit 6 and what is the @TestClassOrder/@TestMethodOrder inheritance change? 11. What is @ParameterizedClass in JUnit 6 and how does it differ from @ParameterizedTest? 12. What is the FastCSV migration in JUnit 6 and how does it affect @CsvSource and @CsvFileSource? 13. What modules were removed in JUnit 6 and what replaced them? 14. How do you migrate from JUnit 5 to JUnit 6? 15. What are assumptions in JUnit 6 and how do they differ from assertions? 16. What is @TestInstance and how does it change test class lifecycle? 17. What is the @RegisterExtension annotation and how does it differ from @ExtendWith? 18. What is the @TempDir annotation in JUnit 6? 19. What are dynamic tests in JUnit 6 and how do you create them with @TestFactory? 20. How does parallel test execution work in JUnit 6? 21. What is @ParameterizedClass in JUnit 6 and how does it differ from @ParameterizedTest? 22. What FastCSV migration happened in JUnit 6 and what are the breaking changes? 23. What is the TestInstance lifecycle in JUnit 6 and what are the two modes? 24. How does test ordering work in JUnit 6 with @TestMethodOrder? 25. What modules were removed in JUnit 6 and why? 26. How do you migrate from JUnit 5 to JUnit 6? 27. What are assumptions in JUnit 6 and how do they differ from assertions? 28. What is the @TempDir annotation and how has it changed in JUnit 6? 29. How does parallel test execution work in JUnit 6? 30. What is the difference between @ExtendWith and @RegisterExtension in JUnit 6? 31. What is the @RepeatedTest annotation in JUnit 6? 32. What is the ExtensionContext and its Store used for in JUnit 6? 33. What are tags and filtering in JUnit 6 and how are they used? 34. What are dynamic tests in JUnit 6 and how does @TestFactory work? 35. How does JUnit 6 integrate with Maven and Gradle? 36. What is the @Suite API in JUnit 6 and how do you group tests into suites? 37. What is the ParameterResolver extension interface and how do you use it for custom injection? 38. What is conditional test execution in JUnit 6 and what built-in conditions are available? 39. What are test interfaces and default methods in JUnit 6? 40. How does JUnit 6 interact with Mockito and Spring Test? 41. What is the TestExecutionListener SPI in JUnit 6? 42. What are the key differences between JUnit 4 and JUnit 6? 43. What is TestWatcher extension interface in JUnit 6 and when do you use it? 44. What is the ConsoleLauncher and how do you run JUnit 6 tests from the command line? 45. What is the @AutoClose extension and how does it simplify resource management in JUnit 6? 46. How does JUnit 6 support for Kotlin differ from JUnit 5? 47. What is the @DisplayNameGeneration annotation in JUnit 6? 48. What is TestReporter in JUnit 6 and how do you use it? 49. What are the unified version changes in JUnit 6 and why do they matter for dependency management? 50. What are best practices and anti-patterns in JUnit 6 test design?
Could not find what you were looking for? send us the question and we would be happy to answer your question.

1. What is JUnit 6 and when was it released?

JUnit 6 is the current major version of the JUnit testing framework for Java and the JVM, released as GA on September 30, 2025 -- eight years after JUnit 5. The latest stable release is 6.1.1 (mid-2026). Unlike the disruptive JUnit 4 to 5 migration (which rewrote the entire annotation model), JUnit 6 is a deliberate maturation rather than a revolution.

JUnit 6 is built on the same Jupiter architecture introduced in JUnit 5 and adds:

  • Java 17 minimum baseline (up from Java 8 in JUnit 5)
  • Unified versioning across Platform, Jupiter, and Vintage modules
  • JSpecify nullability annotations across the entire public API
  • Native Kotlin coroutine support (suspend test methods)
  • CancellationToken API for cooperative cancellation and fail-fast execution
  • FastCSV replacing the unmaintained univocity-parsers library
  • Removal of long-deprecated modules (junit-platform-runner, junit-platform-jfr)
JUnit 6 at a glance
PropertyDetail
GA release dateSeptember 30, 2025
Latest stable6.1.1 (mid-2026)
Minimum JavaJava 17
Minimum KotlinKotlin 2.2
ArchitectureJupiter engine (same as JUnit 5)
Migration from JUnit 5Mostly a version bump; same annotation model
When was JUnit 6 released as a GA (generally available) version?
What is the minimum Java version required by JUnit 6?

2. What is the JUnit 6 architecture and what are its three main components?

JUnit 6 retains the three-tier architecture introduced in JUnit 5, with one key change: all three components now share a single unified version number instead of having separate version schemes.

JUnit 6 three-component architecture
ComponentArtifact prefixRole
JUnit Platformjunit-platform-*Foundation: defines TestEngine SPI, launcher API, and the mechanism for discovering and executing tests. IDEs and build tools integrate at this layer.
JUnit Jupiterjunit-jupiter-*Writing tests: provides all annotations (@Test, @BeforeEach, @ParameterizedTest, etc.), assertions, and extension APIs used by developers writing tests.
JUnit Vintagejunit-vintage-*Legacy: runs JUnit 3 and JUnit 4 tests on the JUnit Platform. Deprecated in JUnit 6 -- will be removed in a future major version.
<!-- Maven BOM: single version for all modules in JUnit 6 -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.junit</groupId>
      <artifactId>junit-bom</artifactId>
      <version>6.1.1</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
    <!-- Version resolved by BOM: 6.1.1 -->
  </dependency>
</dependencies>

<!-- JUnit 5 used different version numbers:
     Platform: 1.x.x
     Jupiter:  5.x.x
     Vintage:  5.x.x

     JUnit 6: all three use 6.x.x -->

Key change from JUnit 5: in JUnit 5, the Platform used version 1.x.x while Jupiter and Vintage used 5.x.x. This caused confusion in dependency management. JUnit 6 unifies all three under the same 6.x.x version, making BOM usage straightforward and eliminating version mismatch errors.

In JUnit 5, what version number scheme did the Platform component use?
What is the status of JUnit Vintage in JUnit 6?

3. What are the core annotations in JUnit 6 and what does each do?

The JUnit 6 annotation model is unchanged from JUnit 5 Jupiter -- this is by design, making migration from JUnit 5 to JUnit 6 primarily a version bump rather than an annotation rewrite.

Core JUnit 6 annotations
AnnotationPurposeNotes
@TestMarks a method as a test caseNo parameters; replaces JUnit 4's @Test
@BeforeEachRuns before each test methodReplaces JUnit 4's @Before
@AfterEachRuns after each test methodReplaces JUnit 4's @After
@BeforeAllRuns once before all tests in the classMust be static unless @TestInstance(PER_CLASS)
@AfterAllRuns once after all tests in the classMust be static unless @TestInstance(PER_CLASS)
@DisabledSkips the annotated test or classReplaces JUnit 4's @Ignore
@DisplayNameCustom display name for the testSupports Unicode, emojis, spaces
@NestedMarks an inner class as a nested test classEnables hierarchical test organisation
@TagCategorises tests for filteringReplaces JUnit 4's @Category
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("Order service tests")
class OrderServiceTest {

    OrderService service;

    @BeforeAll
    static void initAll() {
        System.out.println("Runs once before all tests");
    }

    @BeforeEach
    void setUp() {
        service = new OrderService();
    }

    @Test
    @DisplayName("New order has PENDING status")
    void newOrderIsPending() {
        Order order = service.create("item-1", 2);
        assertEquals(Status.PENDING, order.getStatus());
    }

    @Test
    @Disabled("Not implemented yet")
    void cancelledOrderIsRefunded() {
        // TODO
    }

    @AfterEach
    void tearDown() {
        service = null;
    }

    @AfterAll
    static void tearDownAll() {
        System.out.println("Runs once after all tests");
    }
}

Which JUnit 6 annotation replaces JUnit 4's @Ignore?
What constraint applies to @BeforeAll and @AfterAll methods by default in JUnit 6?

4. What assertions does JUnit 6 provide and how do you use them?

JUnit 6 provides a rich set of assertions in org.junit.jupiter.api.Assertions. All methods are static and can be statically imported. Assertions fail the test immediately when the condition is not met (unlike assumptions, which abort silently).

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

class AssertionsDemo {

    @Test
    void basicAssertions() {
        assertEquals(4, 2 + 2);
        assertNotEquals(5, 2 + 2);
        assertTrue(4 > 3);
        assertFalse(4 < 3);
        assertNull(null);
        assertNotNull("hello");
    }

    @Test
    void assertionsWithMessages() {
        // Message is lazily evaluated (supplier) -- no cost if test passes
        assertEquals(4, 2 + 2, () -> "Expected 4 but was " + (2 + 2));
    }

    @Test
    void groupedAssertions() {
        // assertAll: ALL assertions run even if some fail
        // Reports ALL failures, not just the first one
        assertAll("address",
            () -> assertEquals("London",  address.city()),
            () -> assertEquals("UK",      address.country()),
            () -> assertEquals("EC1A 1BB", address.postcode())
        );
    }

    @Test
    void exceptionAssertions() {
        // assertThrows: passes only if the lambda throws the expected type
        Exception ex = assertThrows(IllegalArgumentException.class,
            () -> new Order(null, -1));
        assertEquals("Quantity must be positive", ex.getMessage());
    }

    @Test
    void doesNotThrow() {
        assertDoesNotThrow(() -> new Order("item", 1));
    }

    @Test
    void timeoutAssertions() {
        // Fails if the lambda takes longer than 1 second
        assertTimeout(Duration.ofSeconds(1), () -> Thread.sleep(500));
    }
}

Key difference -- assertAll vs chained assertions: chained assertions stop at the first failure. assertAll runs every assertion and reports all failures in one go, making it far more useful for validating complex objects with multiple fields.

What is the key advantage of assertAll() over writing multiple separate assertEquals() calls?
What does assertThrows() return when the expected exception is thrown?

5. What are parameterized tests in JUnit 6 and how do you write them?

Parameterized tests run the same test method multiple times with different arguments. In JUnit 6 they are written with @ParameterizedTest and a source annotation that provides the data.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;

class ParameterizedDemo {

    // @ValueSource: simple single-argument tests
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5})
    void isPositive(int n) {
        assertTrue(n > 0);
    }

    // @CsvSource: multiple arguments per invocation
    @ParameterizedTest(name = "{index}: add({0}, {1}) = {2}")
    @CsvSource({
        "1, 2, 3",
        "10, 20, 30",
        "-5, 5, 0"
    })
    void additionTest(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }

    // @MethodSource: call a static method returning a Stream
    @ParameterizedTest
    @MethodSource("provideStrings")
    void blankStringsAreRejected(String input) {
        assertThrows(IllegalArgumentException.class,
            () -> new Username(input));
    }

    static Stream<String> provideStrings() {
        return Stream.of("", " ", "  ", null);
    }

    // @EnumSource: test all or selected enum values
    @ParameterizedTest
    @EnumSource(value = Status.class, names = {"PENDING", "PROCESSING"})
    void activeStatusesAreNotFinal(Status s) {
        assertFalse(s.isFinal());
    }

    // @NullSource / @EmptySource / @NullAndEmptySource
    @ParameterizedTest
    @NullAndEmptySource
    void nullAndEmptyAreRejected(String input) {
        assertThrows(IllegalArgumentException.class,
            () -> new Username(input));
    }
}

JUnit 6 display name change: parameterized test names now consistently format arguments as name = value (with spaces around =) instead of JUnit 5's name=value. This affects CI report output and test filtering by name.

Which @ParameterizedTest source annotation is best for testing a method with multiple input values and one expected output, defined inline as CSV strings?
What is the JUnit 6 display name formatting change for @ParameterizedTest arguments compared to JUnit 5?

6. What is the CancellationToken API introduced in JUnit 6?

One of the headline new features in JUnit 6 is the CancellationToken API -- a cooperative cancellation mechanism that allows launchers (Gradle, Maven, IntelliJ, GitHub Actions) to signal a running test suite to stop cleanly, without killing the JVM process.

The problem it solves: in JUnit 5, there was no clean way to abort a long-running test suite mid-run. Build tools had to forcibly kill the JVM, which skipped teardown methods, left database connections open, and could leave the CI environment in an inconsistent state.

// JUnit 6: CancellationToken -- test and extension authors can check
// for cancellation between steps and abort cleanly.

// Extensions can check cancellation:
class CleanupExtension implements BeforeEachCallback {
    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        // Check if the test run has been cancelled before starting setup
        if (context.getCancellationToken().isCancellationRequested()) {
            return;  // skip setup gracefully
        }
        // ... normal setup ...
    }
}

// --fail-fast mode in ConsoleLauncher:
// The first test failure triggers a CancellationToken that propagates
// through the test suite, stopping remaining tests cleanly.
java -jar junit-platform-console-standalone-6.1.1.jar \
    --scan-class-path \
    --fail-fast
// Equivalent in Maven Surefire:
// <configuration>
//   <runOrder>random</runOrder>
//   <forkCount>1</forkCount>
// </configuration>
// (use --fail-at-end or failIfNoTests per tool docs)

Cancellation token behaviour
ScenarioJUnit 5JUnit 6 with CancellationToken
CI cancels long-running buildJVM killed; teardown skipped; resources leakedCooperative signal; @AfterEach and @AfterAll run; connections closed
--fail-fast modeNot availableFirst failure signals cancellation; remaining tests skipped cleanly
Parallel test suite cancelledThreads killed; database in inconsistent stateEach thread checks token between steps; graceful exit
What problem did the JUnit 6 CancellationToken API solve that existed in JUnit 5?
What does the --fail-fast option do in the JUnit 6 ConsoleLauncher?

7. What are JSpecify nullability annotations in JUnit 6 and why do they matter?

JUnit 6 adds JSpecify nullability annotations (@Nullable, @NonNull, @NullMarked) to its entire public API. This is a significant improvement for static analysis, IDE tooling, and Kotlin interoperability.

JSpecify annotations in JUnit 6
AnnotationMeaningWhere applied
@NonNullThe annotated element is never nullMethod parameters and return types that must not be null
@NullableThe annotated element may be nullParameters or return types that can legitimately be null
@NullMarkedAll unannotated types in this scope are treated as non-null by defaultApplied at package, class, or module level to reduce annotation clutter
// JUnit 6 API example with JSpecify annotations:
// (simplified from actual JUnit 6 source)

@NullMarked  // all unannotated types are non-null by default
public class Assertions {

    // @Nullable return type: assertNull can only be called with a
    // value the compiler considers potentially null
    public static void assertNull(@Nullable Object actual) { ... }

    // Non-null enforced: message MUST be non-null
    public static void assertEquals(
        Object expected,
        Object actual,
        String message  // @NonNull from @NullMarked
    ) { ... }
}

// Kotlin benefit: Kotlin compiler recognises JSpecify annotations
// Smart casts work after assertNotNull():
val result: String? = getValue()
assertNotNull(result)        // Kotlin sees NonNull contract
println(result.length)       // No !! needed -- compiler knows non-null

// JUnit 5 problem:
assertNotNull(result)        // JUnit 5 had no contract
println(result!!.length)     // !! still needed -- compiler didnt know

What does @NullMarked do when applied at class or package level in JUnit 6?
How do JSpecify annotations in JUnit 6 benefit Kotlin test code?

8. What is the native Kotlin coroutine support in JUnit 6?

JUnit 6 adds first-class support for Kotlin suspend functions as test and lifecycle methods. In JUnit 5, testing coroutines required wrapping every test in runBlocking { }. JUnit 6 eliminates this boilerplate: the Jupiter engine manages the coroutine context internally.

// JUnit 5 (before): runBlocking required
class NetworkServiceTest {
    @Test
    fun `should fetch user`(): Unit = runBlocking {
        val user = service.fetchUser("user-1")
        assertEquals("Alice", user.name)
    }

    @BeforeEach
    fun setUp(): Unit = runBlocking {
        service = NetworkService()
        service.connect()  // suspend function
    }
}

// JUnit 6 (now): suspend directly supported
class NetworkServiceTest {
    @Test
    suspend fun `should fetch user`() {  // No runBlocking needed!
        val user = service.fetchUser("user-1")
        assertEquals("Alice", user.name)
    }

    @BeforeEach
    suspend fun setUp() {   // Lifecycle methods can also be suspend
        service = NetworkService()
        service.connect()   // suspend function -- no wrapping needed
    }

    @AfterEach
    suspend fun tearDown() {
        service.disconnect()
    }
}

// IMPORTANT NOTE: JUnit 6's suspend support is equivalent to
// runBlocking -- it does NOT replace kotlin-coroutines-test
// for testing concurrent behaviour, StateFlow, etc.
// For full coroutine test control, use kotlinx-coroutines-test's
// runTest in addition to JUnit 6.

What change in JUnit 6 eliminates the need for runBlocking wrappers in Kotlin test methods?
Is JUnit 6's native suspend test support a full replacement for the kotlinx-coroutines-test library?

9. What are the Extension Model and extension points in JUnit 6?

JUnit 6 uses a single, composable Extension Model replacing JUnit 4's fragmented @RunWith, @Rule, and @ClassRule system. Extensions implement one or more callback interfaces corresponding to specific points in the test lifecycle.

Key extension callback interfaces
InterfaceCallback methodWhen it executes
BeforeAllCallbackbeforeAll(ExtensionContext)Before all tests in the class
BeforeEachCallbackbeforeEach(ExtensionContext)Before each test method
BeforeTestExecutionCallbackbeforeTestExecution(ExtensionContext)Just before the test method body
AfterTestExecutionCallbackafterTestExecution(ExtensionContext)Just after the test method body
AfterEachCallbackafterEach(ExtensionContext)After each test method (including cleanup)
AfterAllCallbackafterAll(ExtensionContext)After all tests in the class
TestInstancePostProcessorpostProcessTestInstance(...)After test instance creation
ParameterResolversupportsParameter + resolveParameterInject custom parameters into test methods
// Custom extension: time each test and log slow ones
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(TimingExtension.class)
public @interface Timed { long thresholdMs() default 100; }

public class TimingExtension
        implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    private static final String START = "start_time";

    @Override
    public void beforeTestExecution(ExtensionContext ctx) {
        ctx.getStore(GLOBAL).put(START, System.currentTimeMillis());
    }

    @Override
    public void afterTestExecution(ExtensionContext ctx) {
        long start = ctx.getStore(GLOBAL).remove(START, long.class);
        long elapsed = System.currentTimeMillis() - start;
        Timed ann = ctx.getRequiredTestMethod().getAnnotation(Timed.class);
        if (ann != null && elapsed > ann.thresholdMs()) {
            System.out.printf("[SLOW] %s took %dms%n",
                ctx.getDisplayName(), elapsed);
        }
    }
}

// Usage:
@Test
@Timed(thresholdMs = 200)
void slowIntegrationTest() { ... }

How do you register an extension in JUnit 6 at the method or class level?
What JUnit 6 extension interface would you implement to inject a custom object into a test method parameter?

10. What are nested tests in JUnit 6 and what is the @TestClassOrder/@TestMethodOrder inheritance change?

Nested tests (inner classes annotated with @Nested) allow you to organise tests hierarchically, grouping related tests and sharing setup/teardown context. JUnit 6 introduces a notable change: @TestClassOrder and @TestMethodOrder are now recursively inherited by all @Nested classes, giving deterministic ordering throughout the hierarchy.

import org.junit.jupiter.api.*;

@DisplayName("Shopping cart")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class ShoppingCartTest {

    Cart cart;

    @BeforeEach
    void init() { cart = new Cart(); }

    @Test @Order(1)
    @DisplayName("is empty on creation")
    void isEmpty() { assertTrue(cart.isEmpty()); }

    // @TestMethodOrder(MethodOrderer.OrderAnnotation.class) inherited
    // by @Nested classes in JUnit 6 -- no need to repeat the annotation
    @Nested
    @DisplayName("when items are added")
    class WhenItemsAdded {

        @BeforeEach
        void addItems() { cart.add(new Item("apple", 1.00)); }

        @Test @Order(1)
        @DisplayName("is not empty")
        void isNotEmpty() { assertFalse(cart.isEmpty()); }

        @Test @Order(2)
        @DisplayName("total reflects added items")
        void totalIsCorrect() { assertEquals(1.00, cart.total()); }

        @Nested
        @DisplayName("when item is removed")
        class WhenItemRemoved {

            @BeforeEach
            void remove() { cart.remove("apple"); }

            @Test @Order(1)
            @DisplayName("is empty again")
            void isEmptyAgain() { assertTrue(cart.isEmpty()); }
        }
    }
}

What change did JUnit 6 make to @TestClassOrder and @TestMethodOrder regarding @Nested classes?
What lifecycle methods does a @Nested class have access to from its enclosing class?

11. What is @ParameterizedClass in JUnit 6 and how does it differ from @ParameterizedTest?

@ParameterizedClass is a JUnit 6 (and late JUnit 5) annotation that parameterises an entire test class rather than a single test method. Instead of repeating @ParameterizedTest on every method, the whole class is instantiated multiple times with different argument sets.

// @ParameterizedTest: repeats one method
class OrderTest {
    @ParameterizedTest
    @CsvSource({"PENDING,false", "SHIPPED,true", "CANCELLED,true"})
    void isFinalStatus(Status status, boolean expected) {
        assertEquals(expected, status.isFinal());
    }
    // Problem: if 10 methods need the same data source, you repeat
    // the annotation 10 times
}

// @ParameterizedClass: repeats the WHOLE class (JUnit 6)
@ParameterizedClass
@CsvSource({"PENDING,false", "SHIPPED,true", "CANCELLED,true"})
class StatusTest {

    @Parameter(0) Status status;      // field injection
    @Parameter(1) boolean expectedFinal;

    @Test
    void isFinalStatus() {
        assertEquals(expectedFinal, status.isFinal());
    }

    @Test
    void descriptionIsNotEmpty() {
        assertFalse(status.description().isBlank());
    }

    @Test
    void statusToStringContainsName() {
        assertTrue(status.toString().contains(status.name()));
    }

    // All 3 tests run for each of the 3 CSV rows:
    // 9 total test executions instead of 3x@ParameterizedTest
}

// Display name format in JUnit 6: "status = PENDING, expectedFinal = false"
// JUnit 5 style was: "status=PENDING, expectedFinal=false" (no spaces)

What is the key difference between @ParameterizedTest and @ParameterizedClass?
In @ParameterizedClass, how are the argument values injected into the test class?

12. What is the FastCSV migration in JUnit 6 and how does it affect @CsvSource and @CsvFileSource?

JUnit 6 replaced the univocity-parsers library (which had stopped receiving maintenance) with FastCSV for parsing CSV data in @CsvSource and @CsvFileSource. FastCSV is actively maintained, has better performance, and is stricter about malformed input.

@CsvSource/@CsvFileSource changes in JUnit 6
AspectJUnit 5 (univocity-parsers)JUnit 6 (FastCSV)
MaintenanceUnmaintained libraryActively maintained
Strict parsingSilently accepted some malformed CSVStrict: throws exception on malformed input
Extra chars after quotesAllowed silently: 'foo'INVALIDException thrown - no extra chars after closing quote
lineSeparator attributePresent in @CsvFileSourceRemoved: line separator auto-detected (\r, \n, \r\n)
Header fieldsignoreLeadingAndTrailingWhitespace did not apply to headersNow applies to headers too
commentCharacterNot configurable (# caused conflicts)New commentCharacter attribute added; default still #
// JUnit 5: this was silently accepted (malformed CSV)
@CsvSource({"'foo'INVALID,'bar'"})

// JUnit 6: throws exception -- "extra characters after closing quote"
// Fix: remove the extra characters
@CsvSource({"'foo','bar'"})

// JUnit 5: @CsvFileSource with explicit line separator
@CsvFileSource(resources = "/data.csv", lineSeparator = "\n")
// JUnit 6: lineSeparator removed -- auto-detected
@CsvFileSource(resources = "/data.csv")
// Auto-detection handles \r, \n, and \r\n correctly

// New in JUnit 6: configure commentCharacter
// Previously # was hardcoded and conflicted with some delimiters
@CsvSource(
    value = {"1, 2, 3", "# this is a comment"},
    commentCharacter = '#'   // can now be customised or disabled
)
void additionTest(int a, int b, int expected) { ... }

Why did JUnit 6 replace univocity-parsers with FastCSV for CSV parsing?
What happens in JUnit 6 if @CsvSource data contains extra characters after a closing quote, such as 'foo'INVALID?

13. What modules were removed in JUnit 6 and what replaced them?

JUnit 6 removed several modules that had been deprecated across JUnit 5.x releases. Understanding what was removed and what replaced it is important for migration.

Removed modules in JUnit 6
Removed moduleWhat it didReplacement in JUnit 6
junit-platform-runnerJUnit 4 @RunWith(JUnitPlatform.class) runner for running JUnit Platform tests via JUnit 4 infrastructureUse @Suite and @SelectClasses from junit-platform-suite directly
junit-platform-jfrProvided Java Flight Recorder (JFR) events for test discovery and executionJFR support is now built into the launcher itself; no separate module needed
junit-platform-suite-commonsInternal commons for suite supportMerged into junit-platform-suite-api
MethodOrderer.AlphanumericAlphabetical test method orderingUse MethodOrderer.DEFAULT or MethodOrderer.MethodName
JUnit Vintage (deprecated)Runs JUnit 3/4 testsStill present but deprecated; will be removed in a future major version
// JUnit 5: @RunWith(JUnitPlatform.class) -- REMOVED
@RunWith(JUnitPlatform.class)  // Does not exist in JUnit 6
public class MyJUnit4Suite { ... }

// JUnit 6: use @Suite directly
@Suite
@SelectClasses({OrderTest.class, UserTest.class})
public class MyTestSuite { }

// JUnit 5: MethodOrderer.Alphanumeric -- REMOVED
@TestMethodOrder(MethodOrderer.Alphanumeric.class)  // Compile error in JUnit 6
class OldOrderedTest { ... }

// JUnit 6: use MethodOrderer.MethodName instead
@TestMethodOrder(MethodOrderer.MethodName.class)
class OrderedTest { ... }

// Check for removed APIs before upgrading:
// grep -r "JUnitPlatform\|Alphanumeric\|platform-runner\|platform-jfr" src/

What replaced junit-platform-runner when it was removed in JUnit 6?
Which MethodOrderer was removed in JUnit 6 and what should you use instead?

14. How do you migrate from JUnit 5 to JUnit 6?

For teams already on JUnit 5.14 and Java 17+, the JUnit team describes the JUnit 6 migration as a "routine dependency bump". The annotation model is unchanged; what breaks is removed APIs and stricter CSV parsing.

<!-- Step 1: Update the BOM version in pom.xml -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.junit</groupId>
      <artifactId>junit-bom</artifactId>
      <version>6.1.1</version>  <!-- was 5.14.x -->
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<!-- Step 2: Update Maven Surefire/Failsafe to 3.x -->
<!-- (versions older than 3.0.0 are no longer supported by JUnit 6) -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>3.5.0</version>
</plugin>

<!-- Gradle: -->
// build.gradle.kts
dependencies {
    testImplementation(platform("org.junit:junit-bom:6.1.1"))
    testImplementation("org.junit.jupiter:junit-jupiter")
}
tasks.test {
    useJUnitPlatform()
}

/* Step 3: Fix compiler errors from removed APIs:
   - Replace Alphanumeric with MethodName
   - Remove junit-platform-runner dependency
   - Remove @RunWith(JUnitPlatform.class) usages
   - Fix malformed @CsvSource data (extra chars after quotes)
   - Ensure Java 17+ baseline

   Step 4: Run the test suite and fix any FastCSV-related
   failures from stricter CSV parsing */

What is the primary prerequisite for a smooth JUnit 5 to JUnit 6 migration?
What minimum version of Maven Surefire plugin does JUnit 6 require?

15. What are assumptions in JUnit 6 and how do they differ from assertions?

Assumptions are conditions checked at the start of a test. If an assumption fails, the test is aborted (skipped) rather than failed. Assumptions are used to skip tests that are not meaningful in a particular environment (e.g. running only on Linux, only with network access, only when a feature flag is enabled).

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

class AssumptionsDemo {

    @Test
    void runOnlyOnCi() {
        // Abort (skip) the test if CI environment variable is not set
        assumeTrue("true".equals(System.getenv("CI")),
            "Skipping: not running in CI environment");
        // Test only executes here if the assumption held
        performSlowIntegrationTest();
    }

    @Test
    void runOnlyOnLinux() {
        assumeTrue(System.getProperty("os.name").startsWith("Linux"));
        // OS-specific test code
    }

    @Test
    void assumptionWithSupplierMessage() {
        // Lazy message evaluation
        String env = System.getenv("APP_ENV");
        assumeFalse("production".equals(env),
            () -> "Skipping destructive test in env: " + env);
        cleanDatabase();
    }

    @Test
    void runSubsetOnlyWhenDatabaseAvailable() {
        // assumingThat: run a block only if condition holds
        // but do NOT abort the whole test
        boolean dbAvailable = isDatabaseAvailable();
        assumingThat(dbAvailable, () -> {
            // Only this block is skipped if db is unavailable
            assertDbRecordExists("user-1");
        });
        // This assertion always runs:
        assertFalse(dbAvailable && isReadOnly());
    }
}

Assertion vs Assumption
AspectAssertion (assertXxx)Assumption (assumeXxx)
Failure effectTest FAILS (red)Test ABORTED/skipped (grey)
PurposeVerify correctnessGuard against meaningless environments
Report visibilityAlways shown as failureShown as skipped/aborted
What is the difference in test outcome between a failed assertion and a failed assumption in JUnit 6?
What does assumingThat() do differently from assumeTrue()?

16. What is @TestInstance and how does it change test class lifecycle?

By default, JUnit creates a new instance of the test class for each test method. @TestInstance(Lifecycle.PER_CLASS) changes this so a single instance is shared across all test methods in the class.

// Default: PER_METHOD (new instance per test)
class DefaultLifecycleTest {
    int count = 0;

    @Test void first()  { count++; assertEquals(1, count); }  // pass
    @Test void second() { count++; assertEquals(1, count); }  // pass
    // Each @Test gets its own instance, so count starts at 0 each time
}

// PER_CLASS: single shared instance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SharedInstanceTest {
    int count = 0;

    @Test @Order(1) void first()  { count++; assertEquals(1, count); }
    @Test @Order(2) void second() { count++; assertEquals(2, count); }
    // Same instance: count accumulates across tests

    // Benefits of PER_CLASS:
    // 1. @BeforeAll and @AfterAll can be NON-STATIC
    @BeforeAll
    void setUpAll() {  // no static required!
        database = Database.connect();
    }

    @AfterAll
    void tearDownAll() {  // no static required!
        database.close();
    }

    // 2. Shared expensive state (database, server)
    //    without static fields
    Database database;
}

PER_METHOD vs PER_CLASS
AspectPER_METHOD (default)PER_CLASS
Instances createdOne per test methodOne per test class
@BeforeAll/@AfterAll must bestaticCan be non-static
Test isolationHigh (fresh instance per test)Lower (shared mutable state)
Use caseUnit testsIntegration tests with shared expensive resources
What is the main reason to use @TestInstance(Lifecycle.PER_CLASS)?
With the default PER_METHOD lifecycle, what is the state of instance fields at the start of each test?

17. What is the @RegisterExtension annotation and how does it differ from @ExtendWith?

@ExtendWith registers an extension via a class reference (declarative, at compile time). @RegisterExtension registers an extension via a field instance at runtime, allowing the extension to be configured with constructor parameters.

// @ExtendWith: declarative, no configuration
@ExtendWith(MockitoExtension.class)
class SimpleTest { ... }

// @RegisterExtension: programmatic, configurable
class ConfigurableTest {

    // Static field: extension scope covers ALL tests (like @BeforeAll)
    @RegisterExtension
    static WireMockExtension server =
        WireMockExtension.newInstance()
            .options(WireMockConfiguration.options().port(8089))
            .build();

    // Instance field: extension scope covers EACH test (like @BeforeEach)
    @RegisterExtension
    TempDirectory tempDir = new TempDirectory();

    @Test
    void callsExternalApi() {
        server.stubFor(get("/orders")
            .willReturn(aResponse().withStatus(200)));
        // test code
    }

    @Test
    void writesToTempFile() throws IOException {
        Path file = tempDir.getRoot().resolve("output.txt");
        Files.writeString(file, "hello");
        assertEquals("hello", Files.readString(file));
    }
}

// Key rule: static @RegisterExtension field
// -> extension callbacks run around the class (BeforeAll/AfterAll)
// Instance @RegisterExtension field
// -> callbacks run around each test (BeforeEach/AfterEach)

What is the key advantage of @RegisterExtension over @ExtendWith?
What determines the lifecycle scope of a @RegisterExtension extension in JUnit 6?

18. What is the @TempDir annotation in JUnit 6?

@TempDir is a built-in JUnit 6 extension (from junit-jupiter-api) that creates a temporary directory for a test and automatically deletes it afterwards. It eliminates the boilerplate of creating, using, and deleting temp directories manually.

import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
import java.io.File;

class TempDirTest {

    // Method parameter injection: per-test temp directory
    @Test
    void writesAndReadsFile(@TempDir Path tempDir) throws Exception {
        Path file = tempDir.resolve("report.txt");
        Files.writeString(file, "Test output");
        assertEquals("Test output", Files.readString(file));
        // Directory is automatically deleted after the test
    }

    // Field injection: same directory reused across tests in class
    @TempDir
    static Path sharedTempDir;  // static = shared across all tests

    @Test
    void firstTestUsesSharedDir() throws Exception {
        Files.writeString(sharedTempDir.resolve("a.txt"), "hello");
    }

    @Test
    void secondTestSeesFilesFromFirst() throws Exception {
        // sharedTempDir persists between tests (static)
        assertTrue(Files.exists(sharedTempDir.resolve("a.txt")));
    }

    // Can also inject as java.io.File:
    @Test
    void withFileType(@TempDir File tempDir) {
        File output = new File(tempDir, "result.csv");
        // ... file operations
    }
}

When @TempDir is injected as a method parameter, what is the scope of the temporary directory?
How do you make a @TempDir shared across all tests in a test class rather than creating a new one per test?

19. What are dynamic tests in JUnit 6 and how do you create them with @TestFactory?

Dynamic tests are tests generated at runtime rather than defined at compile time. They are created using @TestFactory methods that return a Stream, Collection, or Iterable of DynamicTest objects.

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

class DynamicTestsDemo {

    // Basic: generate tests from a list at runtime
    @TestFactory
    Stream<DynamicTest> dynamicAdditionTests() {
        return Stream.of(
            dynamicTest("1 + 1 = 2", () -> assertEquals(2, 1 + 1)),
            dynamicTest("5 + 3 = 8", () -> assertEquals(8, 5 + 3)),
            dynamicTest("-1 + 1 = 0", () -> assertEquals(0, -1 + 1))
        );
    }

    // Useful: load test data from a file, database, or API at runtime
    @TestFactory
    Stream<DynamicTest> testsFromDatabase() {
        return testDataRepository.findAll().stream()
            .map(data -> dynamicTest(
                "Validate: " + data.id(),
                () -> {
                    Result result = service.process(data);
                    assertEquals(data.expectedStatus(), result.status());
                    assertNotNull(result.processedAt());
                }
            ));
    }

    // Dynamic node containers (hierarchical dynamic tests)
    @TestFactory
    Stream<DynamicNode> hierarchical() {
        return Stream.of(
            dynamicContainer("Positive numbers",
                Stream.of(
                    dynamicTest("1 is positive", () -> assertTrue(1 > 0)),
                    dynamicTest("100 is positive", () -> assertTrue(100 > 0))
                )
            )
        );
    }
}

Key distinction: @ParameterizedTest tests are registered at discovery time (the count is known before execution). Dynamic tests are created at runtime -- the count may not be known until the @TestFactory method is called.

What return type must a @TestFactory method have in JUnit 6?
What is the primary use case where @TestFactory dynamic tests excel over @ParameterizedTest?

20. How does parallel test execution work in JUnit 6?

JUnit 6 supports running tests in parallel to reduce total test suite execution time. It must be enabled via configuration and provides fine-grained control over which tests run in parallel.

# Enable parallel execution in junit-platform.properties
# (src/test/resources/junit-platform.properties)

# Enable parallelism globally
junit.jupiter.execution.parallel.enabled=true

# Strategy: dynamic (uses available processors)
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=1.5

# OR: fixed thread count
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=4

# Default mode: all test classes run sequentially,
# all methods within a class run sequentially.
# Override with:
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent

// @Execution annotation: fine-grained control per class or method
import org.junit.jupiter.api.parallel.*;

@Execution(ExecutionMode.CONCURRENT)  // this class runs in parallel
class FastParallelTest {
    @Test void first()  { ... }
    @Test void second() { ... }
}

@Execution(ExecutionMode.SAME_THREAD)  // always sequential
class OrderDependentTest {
    @Test @Order(1) void setup()  { ... }
    @Test @Order(2) void execute(){ ... }
}

// @ResourceLock: prevent concurrent access to shared resources
class DatabaseTest {
    @Test
    @ResourceLock(value = "database", mode = ResourceAccessMode.READ_WRITE)
    void writesToDatabase() { ... }

    @Test
    @ResourceLock(value = "database", mode = ResourceAccessMode.READ)
    void readsFromDatabase() { ... }
    // Reads run concurrently with each other
    // Writes are exclusive (no concurrency with reads or other writes)
}

What configuration property enables parallel test execution in JUnit 6?
What is the purpose of the @ResourceLock annotation in parallel JUnit 6 tests?

21. What is @ParameterizedClass in JUnit 6 and how does it differ from @ParameterizedTest?

@ParameterizedClass (introduced in JUnit 5.13 and fully supported in JUnit 6) parameterises an entire test class rather than a single method. All test methods in the class run for each set of arguments. This is ideal when multiple tests share the same setup conditions that vary per invocation.

import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.api.*;

// @ParameterizedClass: the ENTIRE class runs once per argument
@ParameterizedClass
@MethodSource("locales")
@DisplayName("Cart service in locale")
class CartServiceLocaleTest {

    // Arguments injected via constructor or field
    private final Locale locale;

    CartServiceLocaleTest(Locale locale) {
        this.locale = locale;
    }

    static Stream<Locale> locales() {
        return Stream.of(Locale.US, Locale.UK, Locale.DE);
    }

    @Test
    void currencySymbolIsCorrect() {
        CartService svc = new CartService(locale);
        assertNotNull(svc.getCurrencySymbol());
    }

    @Test
    void priceFormattingMatchesLocale() {
        CartService svc = new CartService(locale);
        String price = svc.format(9.99);
        assertTrue(price.contains(svc.getCurrencySymbol()));
    }
}
// Runs 2 tests x 3 locales = 6 test invocations total

// Compare with @ParameterizedTest (method-level):
class CartServiceTest {
    @ParameterizedTest
    @MethodSource("locales")
    void currencySymbolIsCorrect(Locale locale) {  // single method only
        assertNotNull(new CartService(locale).getCurrencySymbol());
    }
}

@ParameterizedTest vs @ParameterizedClass
Aspect@ParameterizedTest@ParameterizedClass
ScopeSingle test methodEntire test class (all @Test methods)
Arguments injected viaMethod parameterConstructor or field injection
Use caseOne test scenario with multiple inputsMultiple tests sharing the same parameterised setup
What is the key difference between @ParameterizedClass and @ParameterizedTest in JUnit 6?
How are arguments injected into a @ParameterizedClass test class?

22. What FastCSV migration happened in JUnit 6 and what are the breaking changes?

JUnit 6 replaced the univocity-parsers library (used for CSV parsing in @CsvSource and @CsvFileSource) with FastCSV. This was necessary because univocity-parsers became unmaintained. FastCSV is faster and stricter, which causes a few breaking changes for tests with previously tolerated malformed CSV.

FastCSV migration breaking changes
ChangeJUnit 5 behaviourJUnit 6 behaviour
Extra characters after closing quoteSilently toleratedThrows exception: 'foo'INVALID,'bar' is invalid
lineSeparator attribute in @CsvFileSourceConfigurableRemoved: auto-detected (\r, \n, \r\n all work)
Header field processingignoreLeadingAndTrailingWhitespace etc. applied to data onlyNow apply to header fields as well
Comment character in @CsvSource# was default, no way to change itNew commentCharacter attribute; defaults to # but now configurable
Exception messages for malformed CSVImplementation-specificMay differ with FastCSV's error messages
// JUnit 5: this was silently tolerated (malformed CSV)
@CsvSource({"'foo'INVALID,'bar'"})
void test(String a, String b) { ... }
// ^ JUnit 6: throws exception -- extra chars after closing quote

// JUnit 6: lineSeparator removed -- auto-detected
// JUnit 5:
@CsvFileSource(resources = "/data.csv", lineSeparator = "\n")
// JUnit 6:
@CsvFileSource(resources = "/data.csv")  // lineSeparator attribute gone

// JUnit 6: new commentCharacter attribute (needed if # appears in data)
@CsvSource(value = {
    "#this is a comment -- skipped",
    "1, 2, 3",
}, commentCharacter = '#')
void additionTest(int a, int b, int expected) { ... }

// Conflicting delimiter bug fix (from 6.0.1):
// A regression in 6.0.0 caused exception when delimiter=# in @CsvSource
// because # was ALSO the comment character with no way to override.
// 6.0.1 added commentCharacter attribute to fix this.

Why did JUnit 6 replace univocity-parsers with FastCSV?
What happened to the lineSeparator attribute of @CsvFileSource in JUnit 6?

23. What is the TestInstance lifecycle in JUnit 6 and what are the two modes?

By default JUnit 6 creates a new test class instance for each test method (PER_METHOD). The @TestInstance annotation lets you change this to PER_CLASS, where one instance is shared across all methods in the class.

Test instance lifecycle modes
ModeAnnotationBehaviourImplication
PER_METHOD (default)None neededNew instance per @Test methodTest isolation guaranteed; @BeforeAll/@AfterAll must be static
PER_CLASS@TestInstance(Lifecycle.PER_CLASS)Single instance for entire classInstance state can accumulate across tests; @BeforeAll/@AfterAll can be non-static
// PER_METHOD (default): new instance per test
class DefaultLifecycleTest {
    int counter = 0;

    @Test void firstTest()  { counter++; assertEquals(1, counter); }
    @Test void secondTest() { counter++; assertEquals(1, counter); }  // passes!
    // Each test gets a fresh instance with counter=0
}

// PER_CLASS: single shared instance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SharedInstanceTest {
    int counter = 0;

    @Test void firstTest()  { counter++; assertEquals(1, counter); }
    @Test void secondTest() { counter++; assertEquals(2, counter); }  // passes!
    // Shared instance: counter carries over

    // @BeforeAll and @AfterAll can be non-static:
    @BeforeAll
    void initAll() {
        System.out.println("Runs once, non-static -- only possible with PER_CLASS");
    }
}

// PER_CLASS is common for @Nested tests where
// the outer class must hold shared state accessed by inner classes.

Why can @BeforeAll and @AfterAll methods be non-static when using @TestInstance(Lifecycle.PER_CLASS)?
What is the default TestInstance lifecycle in JUnit 6?

24. How does test ordering work in JUnit 6 with @TestMethodOrder?

By default JUnit 6 uses a deterministic but non-obvious method order. @TestMethodOrder lets you explicitly control execution order. In JUnit 6, this annotation is recursively inherited by @Nested classes.

Available MethodOrderer implementations
OrdererBehaviour
MethodOrderer.OrderAnnotationMethods run in @Order(n) value order (ascending)
MethodOrderer.DisplayNameMethods run in display name alphanumeric order
MethodOrderer.MethodNameMethods run in method name alphanumeric order
MethodOrderer.RandomMethods run in random order (with optional seed for reproducibility)
MethodOrderer.AlphanumericRemoved in JUnit 6: replaced by DisplayName or MethodName
import org.junit.jupiter.api.*;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedTest {

    @Test @Order(1) void firstStep()  { System.out.println("1"); }
    @Test @Order(2) void secondStep() { System.out.println("2"); }
    @Test @Order(3) void thirdStep()  { System.out.println("3"); }

    // JUnit 6: @TestMethodOrder is inherited by @Nested classes
    @Nested
    class NestedSteps {
        // No @TestMethodOrder needed -- inherited from outer class
        @Test @Order(1) void nestedFirst()  { System.out.println("N1"); }
        @Test @Order(2) void nestedSecond() { System.out.println("N2"); }
    }
}

// Random order with seed (reproducible):
@TestMethodOrder(MethodOrderer.Random.class)
class RandomOrderTest {
    // Set seed for reproducible random:
    // junit.jupiter.execution.order.random.seed=42
    // in junit-platform.properties
}

Breaking change: MethodOrderer.Alphanumeric was deprecated in JUnit 5 and removed in JUnit 6. Use MethodOrderer.DisplayName or MethodOrderer.MethodName instead.

Which MethodOrderer was removed in JUnit 6 after being deprecated in JUnit 5?
In JUnit 6, what is the significance of @TestMethodOrder being recursively inherited by @Nested classes?

25. What modules were removed in JUnit 6 and why?

JUnit 6 removed several modules that had been deprecated in earlier JUnit 5.x releases. Knowing what was removed and its replacement is a common interview topic.

Modules removed in JUnit 6
Removed moduleWhat it didReplacement
junit-platform-runnerJUnit 4 @RunWith-based runner that could run JUnit Platform tests on JUnit 4 enginesMigrate to native JUnit 6 engine; use Maven Surefire 3.x or Gradle's JUnit Platform support directly
junit-platform-jfrProvided custom Java Flight Recorder (JFR) events for test discovery and executionJFR integration is now built into the launcher itself -- no separate module needed
junit-platform-suite-commonsInternal helper module for test suite supportFunctionality merged into junit-platform-suite
junit-jupiter-migrationsupportCompatibility layer for JUnit 4 @Rule adapters in JUnit 5Deprecated in JUnit 6.0.0; will be removed in the next major version
junit-vintage-engine (deprecated, not removed)Runs JUnit 3/4 tests on the JUnit PlatformStill present but deprecated; remove JUnit 3/4 tests or use an interim Vintage dependency
<!-- BEFORE (JUnit 5 with platform runner) -->
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-runner</artifactId>  <!-- REMOVED in JUnit 6 -->
    <scope>test</scope>
</dependency>

<!-- AFTER (JUnit 6): use Maven Surefire 3.x directly -->
<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.3.0</version>  <!-- Maven Surefire 3.x required for JUnit 6 -->
</plugin>
<!-- Surefire 3.x discovers JUnit Platform tests natively
     without needing junit-platform-runner -->

<!-- jupiter-migrationsupport: deprecated but still present in 6.0.x
     Remove before JUnit 7 ships -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-migrationsupport</artifactId>  <!-- DEPRECATED -->
    <scope>test</scope>
</dependency>

What replaced the functionality of the removed junit-platform-jfr module in JUnit 6?
Why was junit-platform-runner removed in JUnit 6?

26. How do you migrate from JUnit 5 to JUnit 6?

Migrating from JUnit 5 to JUnit 6 is significantly simpler than the JUnit 4 to 5 migration. Because the Jupiter annotation model is unchanged, most projects require only a version bump plus addressing a handful of breaking changes.

JUnit 5 to JUnit 6 migration checklist
StepAction
1. Update dependenciesChange junit-bom version from 5.x.x to 6.1.1; remove explicit version overrides
2. Java 17 baselineEnsure the project targets Java 17 or higher (JUnit 6 requires it)
3. Remove deleted modulesRemove junit-platform-runner and junit-platform-jfr dependencies
4. Fix CSV testsReview @CsvSource and @CsvFileSource tests for malformed CSV (FastCSV is stricter)
5. Remove lineSeparatorRemove lineSeparator attribute from @CsvFileSource (no longer exists)
6. Fix Alphanumeric orderReplace MethodOrderer.Alphanumeric with DisplayName or MethodName
7. Update build toolsUpgrade to Maven Surefire 3.x and Gradle 8.x for JUnit 6 support
8. Update KotlinUpgrade to Kotlin 2.2+ minimum if using Kotlin tests
9. Kotlin suspend testsRemove runBlocking wrappers from suspend test methods
10. Review migrationsupportRemove junit-jupiter-migrationsupport usage (deprecated)
11. Unified versionThe Platform version is now 6.x.x, same as Jupiter -- update any explicit references
<!-- Maven migration: change the BOM version -->
<!-- BEFORE (JUnit 5) -->
<dependency>
    <groupId>org.junit</groupId>
    <artifactId>junit-bom</artifactId>
    <version>5.11.4</version>  <!-- old -->
    <type>pom</type><scope>import</scope>
</dependency>

<!-- AFTER (JUnit 6) -->
<dependency>
    <groupId>org.junit</groupId>
    <artifactId>junit-bom</artifactId>
    <version>6.1.1</version>   <!-- updated -->
    <type>pom</type><scope>import</scope>
</dependency>

<!-- Kotlin tests: remove runBlocking -->
// BEFORE
@Test
fun fetchUser(): Unit = runBlocking {
    val u = service.fetchUser("1")
    assertEquals("Alice", u.name)
}

// AFTER
@Test
suspend fun fetchUser() {
    val u = service.fetchUser("1")
    assertEquals("Alice", u.name)
}

What is the minimum Java version you must target before migrating to JUnit 6?
Which of these migration steps is NOT required when moving from JUnit 5 to JUnit 6?

27. What are assumptions in JUnit 6 and how do they differ from assertions?

Assumptions abort a test silently (marking it as aborted) when a condition is not met. Assertions fail the test with a failure when a condition is not met. Use assumptions to skip tests that are irrelevant in the current environment rather than to verify business logic.

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

class AssumptionsDemo {

    @Test
    void runOnlyOnLinux() {
        assumeTrue(System.getProperty("os.name").toLowerCase().contains("linux"),
            "Skipped: not running on Linux");
        // Test body: only executes on Linux
        // On Windows/macOS: test is ABORTED (not failed)
    }

    @Test
    void runOnlyInCiEnvironment() {
        // assumingThat: execute body only if condition true; continue otherwise
        assumingThat(
            "CI".equals(System.getenv("ENV")),
            () -> {
                // This block runs only in CI
                verifyDeploymentHealth();
            }
        );
        // This line always runs regardless of the assumption
        assertEquals(1, 1);
    }

    @Test
    void skipOnNullDatabase() {
        assumeFalse(System.getenv("DB_URL") == null,
            "Skipped: DB_URL not configured");
        // Integration test body
    }
}

Assumptions vs Assertions comparison
AspectAssumptionsAssertions
Failure modeTest aborted (grey in reports)Test failed (red in reports)
Use caseSkip irrelevant test in current environmentVerify correctness of production logic
MethodsassumeTrue, assumeFalse, assumingThatassertEquals, assertTrue, assertThrows etc.
Effect on suiteSkipped: does not count as pass or failFailure: counts against the build
What is the result of a test when an assumption fails in JUnit 6?
What does assumingThat() do differently from assumeTrue()?

28. What is the @TempDir annotation and how has it changed in JUnit 6?

@TempDir creates a temporary directory for a test and automatically deletes it after the test completes. JUnit 6 adds a cleanup attribute to control when (and whether) the directory is deleted, enabling inspection of files after a failing test.

import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.io.CleanupMode;

class TempDirDemo {

    // Field injection: directory shared across all tests in class
    @TempDir
    static Path sharedTempDir;

    // Parameter injection: fresh directory per test method
    @Test
    void writeAndReadFile(@TempDir Path tempDir) throws Exception {
        Path file = tempDir.resolve("output.txt");
        Files.writeString(file, "Hello JUnit 6");
        assertEquals("Hello JUnit 6", Files.readString(file));
        // tempDir is automatically deleted after this test
    }

    // JUnit 6: cleanup attribute -- control when directory is deleted
    @Test
    void keepDirOnFailure(
        @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path debugDir
    ) throws Exception {
        Files.writeString(debugDir.resolve("log.txt"), "debug info");
        // If test PASSES: directory is deleted
        // If test FAILS:  directory is KEPT for inspection
    }

    // cleanup modes:
    // ALWAYS (default): always delete
    // ON_SUCCESS:       keep on failure (useful for debugging)
    // NEVER:            never delete (manual cleanup)
}

@TempDir cleanup modes (JUnit 6)
ModeWhen deleted
CleanupMode.ALWAYSAlways deleted after test (default)
CleanupMode.ON_SUCCESSOnly deleted if test passes; kept on failure for debugging
CleanupMode.NEVERNever automatically deleted
What new feature does JUnit 6 add to the @TempDir annotation?
What is the practical benefit of CleanupMode.ON_SUCCESS in @TempDir?

29. How does parallel test execution work in JUnit 6?

JUnit 6 supports parallel test execution at both the class and method level. It is disabled by default and is configured via junit-platform.properties or programmatic launcher configuration. The implementation uses Java's ForkJoinPool.

# junit-platform.properties (in src/test/resources/)

# Enable parallel execution
junit.jupiter.execution.parallel.enabled=true

# Strategy: DYNAMIC (based on CPU count) or FIXED (explicit thread count)
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=2
# thread count = processor count * factor  (e.g. 4 cores * 2 = 8 threads)

# OR: fixed thread count
# junit.jupiter.execution.parallel.config.strategy=fixed
# junit.jupiter.execution.parallel.config.fixed.parallelism=4

# Default execution mode: SAME_THREAD (sequential) or CONCURRENT
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent

// Fine-grained control with @Execution:
import org.junit.jupiter.api.parallel.*;

@Execution(ExecutionMode.CONCURRENT)  // This class runs in parallel
class ParallelTests {

    @Test void test1() { /* can run in parallel with test2 */ }
    @Test void test2() { /* can run in parallel with test1 */ }
}

@Execution(ExecutionMode.SAME_THREAD)  // This class runs sequentially
class SequentialTests {
    // Useful for tests with shared mutable state or DB transactions
}

// @ResourceLock: prevent concurrent access to named resources
@ResourceLock(value = "database", mode = ResourceAccessMode.READ_WRITE)
void dbWriteTest() { /* exclusive DB access */ }

@ResourceLock(value = "database", mode = ResourceAccessMode.READ)
void dbReadTest() { /* shared read access -- can run with other READ tests */ }

What Java concurrency mechanism does JUnit 6 use internally for parallel test execution?
What does the @ResourceLock annotation with ResourceAccessMode.READ_WRITE do in parallel execution?

30. What is the difference between @ExtendWith and @RegisterExtension in JUnit 6?

JUnit 6 provides two ways to register extensions. The choice between them depends on whether you need programmatic construction (with constructor arguments) or simple declarative annotation use.

@ExtendWith vs @RegisterExtension
Aspect@ExtendWith@RegisterExtension
LocationAnnotation on class or methodField in the test class
Extension constructionJUnit calls default no-arg constructorDeveloper constructs with custom arguments
When to useStateless extensions with no configurationExtensions that need constructor parameters or runtime configuration
Lifecycle controlLimitedFull: static field = class scope; instance field = method scope
Example@ExtendWith(MockitoExtension.class)@RegisterExtension static WireMockExtension wm = WireMockExtension.newInstance().port(8080).build();
// @ExtendWith: declarative, no constructor args
@ExtendWith(MockitoExtension.class)
class MockitoTest {
    @Mock
    UserRepository repo;

    @Test
    void test() { ... }
}

// @RegisterExtension: programmatic, with constructor args
class WireMockTest {

    // Static field -> class-scoped lifecycle (like @BeforeAll/@AfterAll)
    @RegisterExtension
    static WireMockExtension wireMock = WireMockExtension.newInstance()
        .options(wireMockConfig().port(8080))
        .build();

    // Instance field -> method-scoped lifecycle (fresh per test)
    @RegisterExtension
    DatabaseExtension db = new DatabaseExtension("jdbc:h2:mem:test");

    @Test
    void callsExternalService() {
        wireMock.stubFor(get("/api/users").willReturn(okJson("[]")));
        // ...
    }
}

// Multiple extensions can be combined:
@ExtendWith({MockitoExtension.class, SpringExtension.class})
class SpringMockitoTest { ... }

When should you prefer @RegisterExtension over @ExtendWith?
What is the lifecycle difference between a static @RegisterExtension field and an instance (non-static) @RegisterExtension field?

31. What is the @RepeatedTest annotation in JUnit 6?

@RepeatedTest(n) runs a test method exactly n times. Each repetition is treated as an independent test. It is useful for verifying non-deterministic behaviour, testing concurrency, or stress-testing time-sensitive operations.

import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;

class RepeatedTestDemo {

    // Simple: run exactly 5 times
    @RepeatedTest(5)
    void repeatedTest() {
        assertTrue(Math.random() >= 0.0);
    }

    // Custom display name with placeholders:
    // {displayName}, {currentRepetition}, {totalRepetitions}
    @RepeatedTest(
        value = 5,
        name = "Repetition {currentRepetition} of {totalRepetitions}"
    )
    void namedRepetitions() {
        // Runs 5 times with names: "Repetition 1 of 5", "Repetition 2 of 5"...
    }

    // Inject RepetitionInfo to know which repetition you are in:
    @RepeatedTest(3)
    void withRepetitionInfo(RepetitionInfo info) {
        System.out.printf("Running repetition %d of %d%n",
            info.getCurrentRepetition(),
            info.getTotalRepetitions());

        // Common pattern: fail only on last repetition
        // to verify eventual consistency:
        if (info.getCurrentRepetition() == info.getTotalRepetitions()) {
            assertEquals("done", checkEventualState());
        }
    }
}

// Common use cases for @RepeatedTest:
// - Flaky test isolation (run 10x to catch intermittent failures)
// - Cache warm-up tests (first invocation is slow, later are fast)
// - Thread safety tests (repeated concurrent invocations)
// - Eventual consistency checks (state converges by Nth repetition)

What parameter can you inject into a @RepeatedTest method to know the current repetition number?
What is @RepeatedTest most commonly used for?

32. What is the ExtensionContext and its Store used for in JUnit 6?

The ExtensionContext is the primary object through which extensions interact with the JUnit 6 runtime. It provides access to the current test method, class, display name, tags, and a key-value Store for sharing data between extension callbacks.

// ExtensionContext.Store: share data between callbacks within an extension
public class TimingExtension
        implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    // Namespace prevents collisions between multiple extensions
    private static final Namespace NS =
        Namespace.create(TimingExtension.class);

    @Override
    public void beforeTestExecution(ExtensionContext ctx) {
        // Store the start time under this test's context
        getStore(ctx).put("startTime", System.currentTimeMillis());
    }

    @Override
    public void afterTestExecution(ExtensionContext ctx) {
        long startTime = getStore(ctx).remove("startTime", long.class);
        long elapsed   = System.currentTimeMillis() - startTime;
        System.out.printf("%s took %d ms%n", ctx.getDisplayName(), elapsed);
    }

    private ExtensionContext.Store getStore(ExtensionContext ctx) {
        return ctx.getStore(NS);
    }
}

// ExtensionContext key methods:
// ctx.getDisplayName()           -- test display name
// ctx.getRequiredTestMethod()    -- the @Test Method object
// ctx.getRequiredTestClass()     -- the test class
// ctx.getTags()                  -- Set<String> of @Tag values
// ctx.getTestInstance()          -- Optional<Object> test instance
// ctx.getCancellationToken()     -- JUnit 6: cooperative cancellation
// ctx.getStore(namespace)        -- scoped key-value store
// ctx.getRoot()                  -- root context (global scope)

Why does ExtensionContext.Store use a Namespace in JUnit 6?
What new method does JUnit 6 add to ExtensionContext compared to JUnit 5?

33. What are tags and filtering in JUnit 6 and how are they used?

Tags categorise tests. At build time you can include or exclude specific tags, enabling selective test execution (e.g. run only fast tests in a pre-commit hook, all tests in CI).

// Annotate tests with @Tag
@Tag("fast")
@Tag("unit")
class FastUnitTest {
    @Test void test1() { ... }
}

@Tag("slow")
@Tag("integration")
class SlowIntegrationTest {
    @Test void dbTest() { ... }
}

// Composed annotation to avoid repetition:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Tag("unit")
public @interface UnitTest { }

@UnitTest  // same as @Tag("fast") @Tag("unit")
class OrderServiceTest { ... }

<!-- Maven Surefire: run only fast unit tests -->
<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.3.0</version>
    <configuration>
        <groups>fast & unit</groups>
        <excludedGroups>slow | integration</excludedGroups>
    </configuration>
</plugin>

<!-- ConsoleLauncher: tag expression syntax -->
java -jar junit-platform-console-standalone-6.1.1.jar \
    --scan-class-path \
    --include-tag "fast" \
    --exclude-tag "slow"

# Gradle: filter by tag
test {
    useJUnitPlatform {
        includeTags "fast"
        excludeTags "slow", "integration"
    }
}

What is a composed annotation in the context of JUnit 6 tags?
In Maven Surefire, what configuration element specifies which tags to include when running JUnit 6 tests?

34. What are dynamic tests in JUnit 6 and how does @TestFactory work?

Dynamic tests are generated at runtime rather than being declared statically as annotated methods. The @TestFactory method returns a collection or stream of DynamicTest or DynamicContainer objects, each with its own name and executable.

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

class DynamicTestDemo {

    // @TestFactory: generates tests at runtime from a Stream
    @TestFactory
    Stream<DynamicTest> dynamicTestsFromStream() {
        return Stream.of("apple", "banana", "cherry")
            .map(fruit ->
                dynamicTest(
                    "Is non-empty: " + fruit,
                    () -> assertFalse(fruit.isEmpty())
                )
            );
    }

    // Generate tests from external data source (database, file)
    @TestFactory
    Stream<DynamicTest> testsFromDatabase() {
        return testDataRepository.findAll().stream()
            .map(scenario -> dynamicTest(
                scenario.getName(),
                () -> {
                    Result result = service.process(scenario.getInput());
                    assertEquals(scenario.getExpected(), result);
                }
            ));
    }

    // DynamicContainer: nested dynamic structure
    @TestFactory
    Stream<DynamicNode> nestedDynamic() {
        return Stream.of("cats", "dogs")
            .map(category -> DynamicContainer.dynamicContainer(
                "Tests for " + category,
                Stream.of(
                    dynamicTest("has species", () -> assertNotNull(category)),
                    dynamicTest("is not empty", () -> assertFalse(category.isEmpty()))
                )
            ));
    }
}

What does a @TestFactory method return in JUnit 6?
What is the key difference between @TestFactory dynamic tests and @ParameterizedTest?

35. How does JUnit 6 integrate with Maven and Gradle?

JUnit 6 requires updated build tool versions that support the JUnit Platform. Maven needs Surefire 3.x and Gradle needs 8.x.

<!-- Maven: complete JUnit 6 setup -->
<properties>
    <java.version>17</java.version>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>6.1.1</version>
            <type>pom</type><scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.3.0</version>  <!-- 3.x required for JUnit 6 -->
        </plugin>
    </plugins>
</build>

// Gradle: complete JUnit 6 setup (build.gradle.kts)
plugins {
    java
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

dependencies {
    testImplementation(platform("org.junit:junit-bom:6.1.1"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
    useJUnitPlatform()          // Required: tells Gradle to use JUnit Platform
    maxParallelForks = Runtime.getRuntime().availableProcessors()
}

What Maven Surefire plugin version is required to run JUnit 6 tests?
What Gradle task configuration is required to enable JUnit 6 test discovery in Gradle 8.x?

36. What is the @Suite API in JUnit 6 and how do you group tests into suites?

JUnit 6 provides a @Suite annotation (in junit-platform-suite) for declaratively grouping tests from multiple classes or packages. This replaces JUnit 4's @RunWith(Suite.class) and JUnit Platform Runner (which was removed in JUnit 6).

import org.junit.platform.suite.api.*;

// Basic suite: select specific test classes
@Suite
@SelectClasses({
    OrderServiceTest.class,
    PaymentServiceTest.class,
    InventoryServiceTest.class
})
class SmokeTestSuite {}

// Suite by package (runs all tests in the package)
@Suite
@SelectPackages({"com.example.unit", "com.example.integration"})
class AllTestsSuite {}

// Suite with tag filtering
@Suite
@SelectPackages("com.example")
@IncludeTags("fast")
@ExcludeTags({"slow", "integration"})
class FastTestSuite {}

// Suite with class name pattern
@Suite
@SelectPackages("com.example")
@IncludeClassNamePatterns(".*IT")   // only classes ending in IT
class IntegrationTestSuite {}

// Maven: run suites explicitly
// <configuration>
//   <includes>
//     <include>**/*Suite.class</include>
//   </includes>
// </configuration>

Important: suite classes themselves contain no test methods. They are purely declarative configuration classes. The @Suite annotation is a signal to the JUnit Platform to aggregate and run the selected tests.

Why was the @Suite API needed in JUnit 6 when junit-platform-runner was removed?
What annotation do you use in a @Suite class to include only tests tagged with 'fast'?

37. What is the ParameterResolver extension interface and how do you use it for custom injection?

The ParameterResolver extension interface allows you to inject custom objects into test method parameters, @BeforeEach methods, and constructors. This is the foundation of how MockitoExtension injects @Mock objects and how SpringExtension injects beans.

// Custom ParameterResolver: inject a configured HttpClient
public class HttpClientExtension
        implements ParameterResolver, BeforeEachCallback, AfterEachCallback {

    private final Map<ExtensionContext, HttpClient> clients = new ConcurrentHashMap<>();

    @Override
    public boolean supportsParameter(
            ParameterContext paramCtx,
            ExtensionContext extCtx) {
        // Only resolve HttpClient parameters
        return paramCtx.getParameter().getType() == HttpClient.class;
    }

    @Override
    public Object resolveParameter(
            ParameterContext paramCtx,
            ExtensionContext extCtx) {
        // Create and return an HttpClient for this test
        HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(5))
            .build();
        clients.put(extCtx, client);
        return client;
    }

    @Override
    public void afterEach(ExtensionContext ctx) {
        HttpClient client = clients.remove(ctx);
        if (client != null) client.close();
    }
}

// Usage:
@ExtendWith(HttpClientExtension.class)
class ApiIntegrationTest {

    @Test
    void fetchUsers(HttpClient client) {  // injected by extension!
        HttpResponse<String> resp = client.send(
            HttpRequest.newBuilder(URI.create("https://api.example.com/users")).build(),
            HttpResponse.BodyHandlers.ofString()
        );
        assertEquals(200, resp.statusCode());
    }
}

What two methods must you implement when creating a ParameterResolver extension in JUnit 6?
What does supportsParameter() return to indicate that the extension should NOT inject a value for a given parameter?

38. What is conditional test execution in JUnit 6 and what built-in conditions are available?

JUnit 6 provides condition annotations that skip or enable tests based on runtime conditions without writing assumption code inside the test body. These are implemented as extensions using the ExecutionCondition interface.

Built-in condition annotations
AnnotationSkips test when...
@EnabledOnOs / @DisabledOnOsRunning on a specific OS (e.g. WINDOWS, LINUX, MAC)
@EnabledOnJre / @DisabledOnJreRunning on a specific Java version range
@EnabledForJreRange / @DisabledForJreRangeJRE is inside or outside a specified version range
@EnabledIfSystemProperty / @DisabledIfSystemPropertyA system property matches (or doesn't match) a regex
@EnabledIfEnvironmentVariableAn environment variable matches a regex
@EnabledIf / @DisabledIfA static boolean method returns false / true
import org.junit.jupiter.api.condition.*;

class ConditionalTests {

    @Test
    @EnabledOnOs(OS.LINUX)
    void linuxOnlyTest() { ... }

    @Test
    @DisabledOnOs({OS.WINDOWS, OS.MAC})
    void notOnWindowsOrMac() { ... }

    @Test
    @EnabledOnJre(JRE.JAVA_21)
    void java21Feature() { ... }

    @Test
    @EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_21)
    void java17to21Only() { ... }

    @Test
    @EnabledIfEnvironmentVariable(named = "ENV", matches = "CI")
    void ciOnlyTest() { ... }

    @Test
    @EnabledIfSystemProperty(named = "db.available", matches = "true")
    void databaseTest() { ... }

    // @EnabledIf: call a custom static method
    @Test
    @EnabledIf("isWeekend")
    void weekendTest() { ... }

    static boolean isWeekend() {
        DayOfWeek day = LocalDate.now().getDayOfWeek();
        return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY;
    }
}

What is the advantage of using @EnabledOnOs over writing assumeTrue(OS is Linux) inside the test body?
What interface does JUnit 6 use internally to implement the condition annotations?

39. What are test interfaces and default methods in JUnit 6?

JUnit 6 supports placing @Test, @BeforeEach, @AfterEach, and other annotations on interface default methods. Test classes implementing the interface automatically inherit those tests and lifecycle methods. This is useful for contract testing and shared test behaviour.

// Define a reusable contract test as an interface
public interface ComparableContract<T extends Comparable<T>> {

    T createEqualValue();
    T createSmallerValue();
    T createLargerValue();

    @Test
    default void reflexive() {
        T val = createEqualValue();
        assertEquals(0, val.compareTo(createEqualValue()));
    }

    @Test
    default void symmetricForEqual() {
        T a = createEqualValue();
        T b = createEqualValue();
        assertEquals(0, a.compareTo(b));
        assertEquals(0, b.compareTo(a));
    }

    @Test
    default void smallerIsLessThanLarger() {
        assertTrue(createSmallerValue().compareTo(createLargerValue()) < 0);
    }
}

// Implementing classes get all contract tests for free:
class StringComparableTest implements ComparableContract<String> {
    @Override public String createEqualValue()   { return "hello"; }
    @Override public String createSmallerValue() { return "apple"; }
    @Override public String createLargerValue()  { return "zebra"; }
    // Also free to add class-specific @Test methods
    @Test void stringsAreCaseSensitive() {
        assertNotEquals(0, "Hello".compareTo("hello"));
    }
}

// Any class implementing ComparableContract gets all 3 contract tests.

What is the main use case for placing @Test annotations on interface default methods in JUnit 6?
When a test class implements an interface that has a @Test default method, what happens?

40. How does JUnit 6 interact with Mockito and Spring Test?

JUnit 6 integrates with major testing frameworks via its Extension Model. Mockito and Spring Test both provide JUnit 6-compatible extensions.

// Mockito with JUnit 6:
// Use MockitoExtension (works identically to JUnit 5)
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    OrderRepository repository;

    @InjectMocks
    OrderService service;

    @Test
    void createsOrder() {
        when(repository.save(any())).thenReturn(new Order("O-1", Status.PENDING));
        Order order = service.create("item-1", 2);
        assertEquals(Status.PENDING, order.getStatus());
        verify(repository).save(any(Order.class));
    }
}

// Spring Boot Test with JUnit 6:
@SpringBootTest
class ApplicationIntegrationTest {
    // SpringExtension is auto-registered by @SpringBootTest

    @Autowired
    OrderService service;

    @Test
    void contextLoads() {
        assertNotNull(service);
    }
}

// Spring with Mockito beans:
@SpringBootTest
class OrderControllerTest {

    @Autowired MockMvc mockMvc;

    @MockBean              // Spring-managed mock -- replaces real bean in context
    OrderRepository repo;

    @Test
    void getOrders() throws Exception {
        when(repo.findAll()).thenReturn(List.of(new Order("O-1", Status.PENDING)));
        mockMvc.perform(get("/orders"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$[0].id").value("O-1"));
    }
}

// Dependency versions for JUnit 6 compatibility (mid-2026):
// Mockito 5.x: compatible with JUnit 6
// Spring Boot 3.x: compatible with JUnit 6
// spring-boot-starter-test auto-includes junit-jupiter

How does @MockBean differ from @Mock when used in a Spring Boot test?
Does @SpringBootTest need @ExtendWith(SpringExtension.class) explicitly in JUnit 6?

41. What is the TestExecutionListener SPI in JUnit 6?

The TestExecutionListener is a Service Provider Interface (SPI) that allows external tools (IDEs, build plugins, reporting frameworks) to observe test execution events without modifying test code. Unlike extensions (which are registered in test code), listeners are registered via ServiceLoader or programmatically through the launcher.

// Implement a custom TestExecutionListener:
public class JsonReportListener implements TestExecutionListener {

    private final List<TestResult> results = new ArrayList<>();

    @Override
    public void testStarted(TestIdentifier id) {
        System.out.println("Starting: " + id.getDisplayName());
    }

    @Override
    public void executionFinished(
            TestIdentifier id,
            TestExecutionResult result) {
        results.add(new TestResult(
            id.getDisplayName(),
            result.getStatus()
        ));
    }

    @Override
    public void testPlanExecutionFinished(TestPlan plan) {
        // Write JSON report after all tests complete
        writeJsonReport(results);
    }
}

// Register via ServiceLoader (META-INF/services):
// File: src/test/resources/META-INF/services/
//   org.junit.platform.launcher.TestExecutionListener
// Content:
//   com.example.JsonReportListener

// Or register programmatically via LauncherDiscoveryRequest:
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder
    .request()
    .selectors(selectPackage("com.example"))
    .build();

Launcher launcher = LauncherFactory.create();
launcher.discover(request);
launcher.execute(request, new JsonReportListener());

What is the key difference between a TestExecutionListener and a JUnit 6 Extension?
How do you register a TestExecutionListener via the ServiceLoader mechanism?

42. What are the key differences between JUnit 4 and JUnit 6?

JUnit 4 and JUnit 6 have fundamentally different architectures, annotation models, and extension mechanisms. This is the most common comparison question in interviews where teams have legacy JUnit 4 code.

JUnit 4 vs JUnit 6 comparison
AspectJUnit 4JUnit 6
Packageorg.junitorg.junit.jupiter.api
Test annotation@Test (org.junit)@Test (org.junit.jupiter.api)
Setup@Before / @BeforeClass@BeforeEach / @BeforeAll
Teardown@After / @AfterClass@AfterEach / @AfterAll
Ignore@Ignore@Disabled
Categorise@Category@Tag
Expected exception@Test(expected=Ex.class)assertThrows()
Timeout@Test(timeout=1000)assertTimeout(Duration.ofSeconds(1), ...)
Extension mechanism@RunWith + @Rule + @ClassRule@ExtendWith (single composable model)
Runners@RunWith(MockitoJUnitRunner.class)@ExtendWith(MockitoExtension.class)
Parameterised@RunWith(Parameterized.class)@ParameterizedTest (inline, no runner needed)
Assert styleAssert.assertEquals()Assertions.assertEquals() or AssertJ
Minimum JavaJava 5Java 17 (JUnit 6)
Which JUnit 6 annotation replaces JUnit 4's @Before (runs before each test method)?
What replaces JUnit 4's @Test(expected=IllegalArgumentException.class) in JUnit 6?

43. What is TestWatcher extension interface in JUnit 6 and when do you use it?

The TestWatcher interface provides callbacks for the four possible test outcomes: succeeded, failed, aborted, and disabled. It is simpler than implementing multiple separate extension interfaces when you just want to react to test results.

import org.junit.jupiter.api.extension.TestWatcher;

public class SlackNotificationWatcher implements TestWatcher {

    @Override
    public void testSuccessful(ExtensionContext ctx) {
        // Optional: log to metrics dashboard
    }

    @Override
    public void testFailed(ExtensionContext ctx, Throwable cause) {
        // Alert the team on test failure
        String testName = ctx.getDisplayName();
        String error    = cause.getMessage();
        slackClient.sendAlert(
            String.format("Test FAILED: %s%nError: %s", testName, error)
        );
    }

    @Override
    public void testAborted(ExtensionContext ctx, Throwable cause) {
        // Log skipped tests (assumption violated)
    }

    @Override
    public void testDisabled(ExtensionContext ctx, Optional<String> reason) {
        // Track @Disabled tests for tech debt reporting
        techDebtTracker.record(ctx.getDisplayName(),
            reason.orElse("No reason given"));
    }
}

// Register:
@ExtendWith(SlackNotificationWatcher.class)
class CriticalPathTest {
    @Test
    void paymentProcessing() { ... }

    @Test
    @Disabled("Awaiting payment gateway fix")
    void refundProcessing() { ... }
}

What are the four outcome callbacks provided by the TestWatcher interface in JUnit 6?
What is the difference between testFailed and testAborted in the TestWatcher interface?

44. What is the ConsoleLauncher and how do you run JUnit 6 tests from the command line?

The ConsoleLauncher (junit-platform-console-standalone) is a self-contained JAR that can discover and run JUnit 6 tests from the command line without Maven or Gradle. It is useful for CI scripts, Docker containers, and quick local runs.

# Download the standalone JAR:
# From: https://repo1.maven.org/maven2/org/junit/platform/
#       junit-platform-console-standalone/6.1.1/
#       junit-platform-console-standalone-6.1.1.jar

# Basic run: scan classpath for tests
java -jar junit-platform-console-standalone-6.1.1.jar \
    --scan-class-path \
    --classpath "target/test-classes:target/classes"

# Select specific class:
java -jar junit-platform-console-standalone-6.1.1.jar \
    --select-class "com.example.OrderServiceTest"

# Select by package:
java -jar junit-platform-console-standalone-6.1.1.jar \
    --scan-class-path \
    --select-package "com.example.unit"

# Filter by tag:
java -jar junit-platform-console-standalone-6.1.1.jar \
    --scan-class-path \
    --include-tag "fast" \
    --exclude-tag "slow"

# Fail fast (JUnit 6 CancellationToken):
java -jar junit-platform-console-standalone-6.1.1.jar \
    --scan-class-path \
    --fail-fast

# Generate XML report (for CI systems like Jenkins):
java -jar junit-platform-console-standalone-6.1.1.jar \
    --scan-class-path \
    --reports-dir ./test-reports

What is the purpose of the junit-platform-console-standalone JAR?
What JUnit 6 feature does the --fail-fast flag in ConsoleLauncher use internally?

45. What is the @AutoClose extension and how does it simplify resource management in JUnit 6?

The @AutoClose annotation (introduced in JUnit 5.11 and fully supported in JUnit 6) automatically calls close() on fields implementing AutoCloseable at the end of the test lifecycle, eliminating the need for @AfterEach/@AfterAll teardown methods for simple resource cleanup.

import org.junit.jupiter.api.AutoClose;

class AutoCloseDemo {

    // Closed after each test method (instance-scoped)
    @AutoClose
    private final Connection db = DriverManager.getConnection(TEST_DB_URL);

    // Closed once after all tests (static = class-scoped)
    @AutoClose
    private static final HttpClient httpClient = HttpClient.newHttpClient();

    @Test
    void queryUsers() throws SQLException {
        // db is open and ready; closed automatically after this test
        try (PreparedStatement ps = db.prepareStatement("SELECT * FROM users")) {
            ResultSet rs = ps.executeQuery();
            assertTrue(rs.next());
        }
    }

    @Test
    void callExternalApi() throws Exception {
        // httpClient is open; closed once after all tests complete
        HttpResponse<String> resp = httpClient.send(
            HttpRequest.newBuilder(URI.create("https://api.test.com")).build(),
            HttpResponse.BodyHandlers.ofString()
        );
        assertEquals(200, resp.statusCode());
    }
}
// No @AfterEach or @AfterAll needed for these resources!

// Equivalent JUnit 5 code (before @AutoClose):
class JUnit5Equivalent {
    private Connection db;
    @BeforeEach void setUp() throws SQLException {
        db = DriverManager.getConnection(TEST_DB_URL);
    }
    @AfterEach void tearDown() throws SQLException {
        if (db != null) db.close();  // <-- boilerplate eliminated by @AutoClose
    }
}

What interface must a field's type implement for @AutoClose to work?
When is a static @AutoClose field closed compared to an instance @AutoClose field?

46. How does JUnit 6 support for Kotlin differ from JUnit 5?

JUnit 6 significantly improves Kotlin support beyond just suspend function tests. The minimum Kotlin version is 2.2 and several common pain points from JUnit 5 Kotlin usage are addressed.

JUnit 6 Kotlin improvements
AreaJUnit 5 pain pointJUnit 6 improvement
suspend testsrunBlocking required everywhereSuspend functions work directly as @Test, @BeforeEach, @AfterEach
NullabilityAPI had no nullability contracts; !! neededJSpecify annotations enable smart casts after assertNotNull
@BeforeAll in objectsRequired @JvmStatic@TestInstance(PER_CLASS) avoids @JvmStatic
kotlin-test interopkotlin.test.assertEquals vs junit.jupiter assertionsJSpecify nullability aligns contracts between both
Minimum versionKotlin 1.6+Kotlin 2.2+ required
// Kotlin JUnit 6 test class
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class KotlinOrderTest {

    private lateinit var service: OrderService

    // No @JvmStatic needed with PER_CLASS lifecycle
    @BeforeAll
    suspend fun setUpAll() {
        service = OrderService()
        service.connect()   // suspend function -- works directly!
    }

    @BeforeEach
    suspend fun setUp() {
        service.reset()     // suspend function
    }

    @Test
    suspend fun `new order has pending status`() {
        val order = service.create("item-1", 2)  // suspend call
        assertEquals(Status.PENDING, order.status)
    }

    @Test
    suspend fun `invalid quantity throws`() {
        assertThrows<IllegalArgumentException> {
            service.create("item-1", -1)   // suspend call
        }
    }

    // JSpecify: smart cast works after assertNotNull
    @Test
    fun `smart cast after assertNotNull`() {
        val result: String? = service.findUser("user-1")
        assertNotNull(result)       // JUnit 6 exposes @NonNull contract
        println(result.length)      // No !! needed -- smart cast works
    }

    @AfterAll
    suspend fun tearDownAll() {
        service.disconnect()        // suspend function
    }
}

What Kotlin language version is the minimum required by JUnit 6?
How does using @TestInstance(PER_CLASS) help Kotlin JUnit 6 tests with @BeforeAll?

47. What is the @DisplayNameGeneration annotation in JUnit 6?

@DisplayNameGeneration automatically generates human-readable display names for all test methods in a class without requiring a @DisplayName on each method. It transforms method names (which must follow identifier rules) into more readable strings.

import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;

// Converts method names to sentences:
// - Removes underscores
// - Removes trailing parentheses
// "it_should_return_empty_when_null()" -> "it should return empty when null"
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class OrderServiceDisplayTest {

    @Test
    void new_order_has_pending_status() { ... }
    // Displayed as: "new order has pending status"

    @Test
    void order_with_invalid_quantity_throws_exception() { ... }
    // Displayed as: "order with invalid quantity throws exception"
}

// IndicativeSentences: generates "ClassName, method name"
@DisplayNameGeneration(DisplayNameGenerator.IndicativeSentences.class)
class PaymentTest {
    @Test
    void processValidPayment() { ... }
    // Displayed as: "PaymentTest, processValidPayment()"
}

// Custom generator:
public class CamelCaseToSentenceGenerator implements DisplayNameGenerator {
    @Override
    public String generateDisplayNameForClass(Class<?> cls) {
        return splitCamelCase(cls.getSimpleName());
    }
    @Override
    public String generateDisplayNameForNestedClass(Class<?> cls) {
        return splitCamelCase(cls.getSimpleName());
    }
    @Override
    public String generateDisplayNameForMethod(Class<?> cls, Method method) {
        return splitCamelCase(method.getName());
    }
    private String splitCamelCase(String name) {
        return name.replaceAll("([A-Z])", " $1").trim().toLowerCase();
    }
}

// Set globally in junit-platform.properties:
// junit.jupiter.displayname.generator.default=
//   org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores

What does DisplayNameGenerator.ReplaceUnderscores do to method names?
How do you set a default @DisplayNameGeneration strategy for all test classes in the project without annotating each class?

48. What is TestReporter in JUnit 6 and how do you use it?

TestReporter lets test methods publish key-value entries to the test execution report. Unlike System.out.println, reported values are attached to the test result and appear in IDE test reports and XML output -- they are not just lost in console noise.

import org.junit.jupiter.api.TestReporter;

class TestReporterDemo {

    @Test
    void reportingDemo(TestReporter reporter) {
        // Publish a single key-value entry
        reporter.publishEntry("user-id", "U-12345");
        reporter.publishEntry("status", "active");

        // Publish a map of entries in one call
        Map<String, String> values = Map.of(
            "order-id",   "O-99",
            "total",      "149.99",
            "currency",   "GBP"
        );
        reporter.publishEntry(values);

        // Assertions still run after publishing entries
        assertTrue(true);
    }

    @Test
    void reportWithMessage(TestReporter reporter) {
        // Single-argument form: just a message string
        reporter.publishEntry("Processing payment for order O-42");
    }
}

// TestReporter vs System.out:
//
// System.out.println:
//   - Goes to process stdout
//   - Not associated with any specific test in the report
//   - Lost in CI log noise
//
// TestReporter.publishEntry:
//   - Attached to the specific test result
//   - Appears in IDE test runner output alongside the test
//   - Included in XML/HTML test reports
//   - Visible when you click on a specific test result in the IDE

What is the advantage of using TestReporter.publishEntry() over System.out.println() in JUnit 6 tests?
How is TestReporter provided to a test method?

49. What are the unified version changes in JUnit 6 and why do they matter for dependency management?

One of the practical improvements in JUnit 6 is the adoption of a single unified version number across all three components. In JUnit 5, the components used different version schemes, causing widespread confusion and dependency conflicts.

JUnit version comparison
ComponentJUnit 5 versionsJUnit 6 versions
JUnit Platform1.0.x through 1.11.x6.0.x through 6.x.x (same as others)
JUnit Jupiter5.0.x through 5.11.x6.0.x through 6.x.x (same as Platform)
JUnit Vintage5.0.x through 5.11.x6.0.x through 6.x.x (same as Platform)
<!-- JUnit 5: confusing mixed versions -->
<!-- Correct combination for JUnit 5.11.4: -->
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-engine</artifactId>
    <version>1.11.4</version>  <!-- Platform uses 1.x.x -->
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.11.4</version>  <!-- Jupiter uses 5.x.x -->
</dependency>
<!-- Common mistake: using 5.11.4 for Platform (wrong!) -->
<!-- or 1.11.4 for Jupiter (also wrong!) -->

<!-- JUnit 6: all use the same version -->
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-engine</artifactId>
    <version>6.1.1</version>   <!-- same version -->
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>6.1.1</version>   <!-- same version -->
</dependency>

<!-- BOM still recommended to avoid managing versions at all -->
<dependency>
    <groupId>org.junit</groupId>
    <artifactId>junit-bom</artifactId>
    <version>6.1.1</version>
    <type>pom</type><scope>import</scope>
</dependency>

What was the version number of JUnit Platform when JUnit Jupiter was at version 5.11.4?
Why does unified versioning in JUnit 6 reduce confusion compared to JUnit 5?

50. What are best practices and anti-patterns in JUnit 6 test design?

Writing maintainable, fast, and reliable tests requires following established principles. Knowing common anti-patterns is as important as knowing the APIs.

JUnit 6 best practices and anti-patterns
CategoryBest practiceAnti-pattern to avoid
Test isolationEach test should set up its own state independentlyRelying on test execution order or shared mutable state between tests
Assertion messagesUse lazy message suppliers: assertEquals(a, b, () -> 'msg')Eager string concatenation in message: assertEquals(a, b, 'msg: ' + val) -- always evaluated
Exception testingUse assertThrows() and verify the message@Test(expected=) which doesn't let you inspect the exception
GroupingUse assertAll() for multi-field object validationMultiple separate assertions -- first failure hides others
NamingUse @DisplayName or @DisplayNameGeneration for readable namescryptic method names like testMethod1()
Extension reuseExtract cross-cutting concerns into extensions or shared flowsCopy-pasting @BeforeEach setup across test classes
Parallel safetyUse @ResourceLock for shared resourcesShared mutable static state without synchronisation
Temporary filesUse @TempDir(cleanup=ON_SUCCESS) for debuggable failuresManual file management in @BeforeEach/@AfterEach
// ANTI-PATTERN: order-dependent tests
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class BadTest {
    static String sharedState;

    @Test @Order(1) void setup()  { sharedState = "ready"; }  // BAD
    @Test @Order(2) void useIt() { assertEquals("ready", sharedState); }
}

// BEST PRACTICE: independent setup per test
class GoodTest {
    @BeforeEach
    void init() { this.state = buildTestState(); }  // each test gets fresh state

    @Test void test1() { assertEquals("ready", state.status()); }
    @Test void test2() { assertEquals("ready", state.status()); }  // also passes
}

// ANTI-PATTERN: testing multiple concerns in one test
@Test
void testEverything() {
    // Tests create, update, delete, and serialise all at once
    // When it fails, you don't know WHAT broke
}

// BEST PRACTICE: one concern per test
@Test void createOrder_succeeds() { ... }
@Test void updateOrder_changesStatus() { ... }
@Test void deleteOrder_removesFromStore() { ... }

Why should you use a lambda supplier for assertion messages instead of a plain String in JUnit 6?
What does the single responsibility principle mean when applied to JUnit 6 test methods?
«
»
API

Comments & Discussions