Tag Archives: Java

JUnit 6 Assertions: All Methods Explained with Real Examples

Assertions are the heart of every JUnit 6 test. They are the statements that determine whether a test passes or fails. JUnit 6 provides a rich set of assertion methods in the org.junit.jupiter.api.Assertions class β€” from simple equality checks to grouped assertions, exception verification, and timeout enforcement. This guide covers every assertion method with real examples, console output, and guidance on when to use each one.

All assertion methods are static, so the standard pattern is to use a static import at the top of your test class:

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

1. assertEquals and assertNotEquals

The most commonly used assertions. They compare two values for equality using .equals():

@Test
@DisplayName("assertEquals and assertNotEquals examples")
void equalityAssertions() {
    String greeting = "Hello, JUnit 6";

    // Basic equality check
    assertEquals("Hello, JUnit 6", greeting);

    // With a custom failure message (shown if assertion fails)
    assertEquals("Hello, JUnit 6", greeting, "Greeting string should match exactly");

    // Lazy message: the Supplier lambda only evaluates if the assertion FAILS
    // Use this when building the message is expensive (e.g. involves I/O)
    assertEquals("Hello, JUnit 6", greeting,
        () -> "Expected greeting to be 'Hello, JUnit 6' but got: " + greeting);

    // assertNotEquals: passes when values are NOT equal
    assertNotEquals("Goodbye", greeting, "Greeting should not be 'Goodbye'");

    // Floating-point comparison with a delta (tolerance)
    // Never compare doubles with == or assertEquals without a delta
    double result = 0.1 + 0.2; // = 0.30000000000000004 due to floating-point
    assertEquals(0.3, result, 0.0001, "0.1 + 0.2 should be approximately 0.3");
}

2. assertTrue and assertFalse

@Test
@DisplayName("assertTrue and assertFalse examples")
void booleanAssertions() {
    List languages = List.of("Java", "Python", "Go");

    // assertTrue: passes when condition is true
    assertTrue(languages.contains("Java"), "List should contain Java");
    assertTrue(languages.size() > 0, "List should not be empty");

    // assertFalse: passes when condition is false
    assertFalse(languages.isEmpty(), "List should not be empty");
    assertFalse(languages.contains("COBOL"), "List should not contain COBOL");

    // Works with Predicate-style lambdas for complex conditions
    assertTrue(languages.stream().anyMatch(lang -> lang.startsWith("J")),
        "At least one language should start with J");
}

3. assertNull and assertNotNull

@Test
@DisplayName("assertNull and assertNotNull examples")
void nullAssertions() {
    String uninitialised = null;
    String initialised = "JUnit 6";

    // assertNull: passes when value IS null
    assertNull(uninitialised, "Uninitialised variable should be null");

    // assertNotNull: passes when value is NOT null
    assertNotNull(initialised, "Initialised variable should not be null");

    // Common real-world usage: verify a factory method returns a non-null object
    Order order = OrderFactory.createDefault();
    assertNotNull(order, "OrderFactory.createDefault() must not return null");
    assertNotNull(order.getId(), "Created order must have an assigned ID");
}
Continue reading JUnit 6 Assertions: All Methods Explained with Real Examples

Writing Your First Clean Test in JUnit 6 (Best Practices)

Writing a test that compiles and passes is easy. Writing a test that is clean, readable, trustworthy, and maintainable takes deliberate practice. This guide walks you through exactly what separates a good JUnit 6 test from a great one β€” structure, naming, assertions, test isolation, and the habits that experienced Java developers rely on every day.

Every example here follows the patterns established in the JUnit 6 Tutorial series, building on the lifecycle and project structure knowledge from earlier chapters.

The Anatomy of a Clean JUnit 6 Test

Every well-written test has three distinct phases, known as the Arrange-Act-Assert (AAA) pattern. Each phase has one job:

  • Arrange β€” Set up the objects, data, and state needed for the test
  • Act β€” Call the method or trigger the behaviour being tested
  • Assert β€” Verify the outcome is what you expected
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class BankAccountTest {

    // ----------------------------------------------------------------
    // CLEAN TEST β€” follows Arrange-Act-Assert with clear separation
    // ----------------------------------------------------------------
    @Test
    @DisplayName("Depositing funds increases balance by the deposited amount")
    void depositingFundsIncreasesBalance() {

        // Arrange: set up the object under test and the input
        BankAccount account = new BankAccount("ACC001", 100.00);
        double depositAmount = 50.00;

        // Act: call the method being tested
        account.deposit(depositAmount);

        // Assert: verify the outcome
        assertEquals(150.00, account.getBalance(), 0.001,
            "Balance should be initial 100 + deposited 50 = 150");
    }

    // ----------------------------------------------------------------
    // CLEAN TEST β€” exception testing with descriptive assertion message
    // ----------------------------------------------------------------
    @Test
    @DisplayName("Depositing a negative amount throws IllegalArgumentException")
    void depositingNegativeAmountThrowsException() {

        // Arrange
        BankAccount account = new BankAccount("ACC001", 100.00);

        // Act + Assert combined: assertThrows captures the exception
        IllegalArgumentException exception = assertThrows(
            IllegalArgumentException.class,
            () -> account.deposit(-10.00),
            "Negative deposit should throw IllegalArgumentException"
        );

        // Assert the exception message contains meaningful information
        assertTrue(exception.getMessage().contains("negative"),
            "Exception message should mention 'negative'");
    }
}
Continue reading Writing Your First Clean Test in JUnit 6 (Best Practices)

JUnit 6 Test Lifecycle Explained (BeforeEach, AfterAll, and More)

The JUnit 6 test lifecycle defines exactly when each setup and teardown method runs relative to your tests. Misunderstanding the lifecycle is one of the most common sources of subtle, order-dependent test failures. This guide covers every lifecycle annotation in depth β€” @BeforeAll, @BeforeEach, @AfterEach, @AfterAll β€” with complete code examples, execution order diagrams, and the important differences between the PER_METHOD and PER_CLASS instance lifecycles.

The Four Lifecycle Annotations

AnnotationRunsMust be static?JUnit 4 equivalent
@BeforeAllOnce before ALL tests in the classYes (default) / No (PER_CLASS)@BeforeClass
@BeforeEachBefore EACH individual test methodNo@Before
@AfterEachAfter EACH individual test methodNo@After
@AfterAllOnce after ALL tests in the classYes (default) / No (PER_CLASS)@AfterClass

Complete Lifecycle Example

This example demonstrates all four annotations together. Read the comments and then check the output to see exactly what order they run in:

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

@DisplayName("Order Processing Lifecycle Demo")
class OrderProcessorTest {

    // Simulates a shared resource (e.g. DB connection, test server)
    private static int sharedResourceInitCount = 0;

    // Instance field: reset before every test because JUnit creates
    // a new instance of this class for each test method (PER_METHOD, default)
    private OrderProcessor orderProcessor;

    // ----------------------------------------------------------------
    // @BeforeAll: runs ONCE before any test in this class.
    // Must be static in the default PER_METHOD lifecycle.
    // Use for expensive one-time setup: starting servers, loading fixtures.
    // ----------------------------------------------------------------
    @BeforeAll
    static void initialiseSharedResources() {
        sharedResourceInitCount++;
        System.out.println("[BeforeAll] Shared resources initialised. Count: " + sharedResourceInitCount);
    }

    // ----------------------------------------------------------------
    // @BeforeEach: runs before EVERY individual @Test method.
    // Use for creating a fresh object under test to avoid state pollution.
    // ----------------------------------------------------------------
    @BeforeEach
    void createFreshOrderProcessor() {
        orderProcessor = new OrderProcessor();
        System.out.println("[BeforeEach] Fresh OrderProcessor created");
    }

    @Test
    @DisplayName("Creating an order sets status to PENDING")
    void creatingOrderSetsPendingStatus() {
        Order order = orderProcessor.createOrder("[email protected]", 49.99);
        System.out.println("[Test] creatingOrderSetsPendingStatus running");
        assertEquals(OrderStatus.PENDING, order.getStatus());
    }

    @Test
    @DisplayName("Completing an order sets status to COMPLETED")
    void completingOrderSetsCompletedStatus() {
        Order order = orderProcessor.createOrder("[email protected]", 49.99);
        orderProcessor.completeOrder(order);
        System.out.println("[Test] completingOrderSetsCompletedStatus running");
        assertEquals(OrderStatus.COMPLETED, order.getStatus());
    }

    // ----------------------------------------------------------------
    // @AfterEach: runs after EVERY individual @Test method.
    // Use for cleanup: closing files, resetting mocks, clearing caches.
    // ----------------------------------------------------------------
    @AfterEach
    void cleanUpAfterTest() {
        orderProcessor = null; // help GC, signal we are done with this instance
        System.out.println("[AfterEach] Cleaned up");
    }

    // ----------------------------------------------------------------
    // @AfterAll: runs ONCE after ALL tests in this class have finished.
    // Must be static in the default PER_METHOD lifecycle.
    // Use for releasing shared resources: closing DB connections, stopping servers.
    // ----------------------------------------------------------------
    @AfterAll
    static void releaseSharedResources() {
        System.out.println("[AfterAll] Shared resources released");
    }
}

Console Output

[BeforeAll] Shared resources initialised. Count: 1

[BeforeEach] Fresh OrderProcessor created
[Test] creatingOrderSetsPendingStatus running
[AfterEach] Cleaned up

[BeforeEach] Fresh OrderProcessor created
[Test] completingOrderSetsCompletedStatus running
[AfterEach] Cleaned up

[AfterAll] Shared resources released

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

Notice that @BeforeAll runs exactly once, @BeforeEach and @AfterEach run twice (once per test), and @AfterAll runs exactly once at the end.

Continue reading JUnit 6 Test Lifecycle Explained (BeforeEach, AfterAll, and More)

JUnit 6 Architecture Deep Dive: Platform, Engines, and Launchers

Most developers use JUnit 6 daily without thinking about what happens under the hood when they click β€œRun.” Understanding the JUnit 6 architecture β€” how the Platform discovers tests, how Engines execute them, and how Launchers tie it all together β€” makes you a dramatically more effective tester. You will debug mysterious β€œ0 tests found” issues in seconds, build custom extensions with confidence, and understand why certain configurations behave the way they do.

The Three-Pillar Architecture

JUnit 6 is not a single monolithic library. It is composed of three distinct modules, each with a clearly defined responsibility:

  • JUnit Platform β€” The foundation. Defines the TestEngine SPI and the Launcher API. Build tools (Maven, Gradle) and IDEs talk to the Platform, not to Jupiter or Vintage directly.
  • JUnit Jupiter β€” The JUnit 6 programming model. Contains all the annotations (@Test, @BeforeEach, etc.), assertion classes, and the JupiterTestEngine that understands them.
  • JUnit Vintage β€” A compatibility shim. Contains the VintageTestEngine which wraps the old JUnit 4 runner so legacy tests can run on the JUnit Platform.
Continue reading JUnit 6 Architecture Deep Dive: Platform, Engines, and Launchers

JUnit 6 vs JUnit 5: Key Differences, Features, and Migration Guide

If you have been using JUnit 5 (also known as JUnit Jupiter) and are evaluating whether to move to JUnit 6, or you just want to understand what changed and why, this guide has every answer. We compare the two frameworks side by side β€” architecture, annotations, APIs, extension model, and migration path β€” with real code examples throughout.

The short answer: JUnit 6 is evolutionary, not revolutionary. If you know JUnit 5, you already know 90% of JUnit 6. The upgrade pays off in cleaner extension APIs, improved parameterized tests, and better alignment with modern Java features like records and sealed classes.

Architecture: What Stayed the Same

Both JUnit 5 and JUnit 6 are built on the same three-pillar architecture:

ModulePurposeJUnit 5JUnit 6
PlatformLaunches test frameworks on the JVMβœ… Presentβœ… Present (refined)
JupiterProgramming model for writing testsβœ… Presentβœ… Present (enhanced)
VintageRuns JUnit 3/4 tests on the Platformβœ… Presentβœ… Present

Side-by-Side Annotation Comparison

All core annotations are identical in name and purpose. JUnit 6 adds refinements, not replacements:

FeatureJUnit 5JUnit 6Change
Test method@Test@TestSame
Before each test@BeforeEach@BeforeEachSame
After each test@AfterEach@AfterEachSame
Before all tests@BeforeAll@BeforeAllSame
After all tests@AfterAll@AfterAllSame
Display name@DisplayName@DisplayNameSame
Nested tests@Nested@NestedSame
Tag@Tag@TagSame
Disable test@Disabled@DisabledSame
Parameterized@ParameterizedTest@ParameterizedTestEnhanced sources
Dynamic tests@TestFactory@TestFactorySame
Extensions@ExtendWith@ExtendWithEnhanced API
Timeout@Timeout@TimeoutSame
Temp directory@TempDir@TempDirSame
Method ordering@TestMethodOrder@TestMethodOrderMore strategies
Continue reading JUnit 6 vs JUnit 5: Key Differences, Features, and Migration Guide

JUnit 6 Project Structure and Best Practices

A well-organised test project is just as important as well-organised production code. Poor test structure leads to duplication, brittle tests, and test suites that are hard to navigate and maintain. This guide shows you exactly how to structure a JUnit 6 project β€” from package layout to naming conventions, test base classes, shared utilities, and the practices that keep test suites healthy at scale.

Whether you are starting fresh or cleaning up an existing project, the patterns here apply to any Java application β€” monolith, microservice, or library.

The Standard Maven/Gradle Directory Layout

JUnit 6 follows the standard Maven directory convention, which Gradle also adopts by default:

my-app/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ main/
β”‚   β”‚   └── java/
β”‚   β”‚       └── com/example/
β”‚   β”‚           β”œβ”€β”€ service/
β”‚   β”‚           β”‚   └── OrderService.java
β”‚   β”‚           β”œβ”€β”€ repository/
β”‚   β”‚           β”‚   └── OrderRepository.java
β”‚   β”‚           └── model/
β”‚   β”‚               └── Order.java
β”‚   └── test/
β”‚       β”œβ”€β”€ java/
β”‚       β”‚   └── com/example/          <-- mirrors main package structure
β”‚       β”‚       β”œβ”€β”€ service/
β”‚       β”‚       β”‚   └── OrderServiceTest.java
β”‚       β”‚       β”œβ”€β”€ repository/
β”‚       β”‚       β”‚   └── OrderRepositoryIT.java  <-- IT suffix = integration test
β”‚       β”‚       └── common/
β”‚       β”‚           └── BaseTest.java           <-- shared test base class
β”‚       └── resources/
β”‚           └── junit-platform.properties  <-- JUnit 6 config
β”œβ”€β”€ pom.xml
└── README.md

Key principles of this layout:

  • Test packages mirror production packages so you can immediately find the test for any class
  • Unit tests end with Test; integration tests end with IT
  • Shared test infrastructure lives in a common sub-package
  • JUnit 6 configuration goes in src/test/resources/junit-platform.properties
Continue reading JUnit 6 Project Structure and Best Practices

JUnit 6 with Maven and Gradle: Complete Setup Guide

Setting up JUnit 6 with Maven and Gradle correctly is more than adding one dependency. You need the right plugin versions, the right configuration flags, and the right test source layout so tests are discovered, executed, and reported correctly every time. This guide covers every configuration option from a basic single-module project to advanced multi-module and CI-ready setups.

If you are brand new to JUnit 6, start with Getting Started with JUnit 6 first. This post goes deeper into build tool specifics.

Part 1: JUnit 6 with Maven

Step 1 β€” The Core Dependency

Add junit-jupiter to your pom.xml. This single aggregator pulls in the API, params module, and engine in one shot:

<properties>
    <junit.version>6.0.0</junit.version>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
</properties>

<dependencies>
    <!-- JUnit Jupiter aggregator: API + Params + Engine -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>${junit.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Step 2 β€” Maven Surefire Plugin (Unit Tests)

The Maven Surefire Plugin runs unit tests during the test phase. You must use version 3.2.5 or higher β€” older versions do not support the JUnit Platform:

<build>
    <plugins>
        <!-- Surefire: runs tests in the 'test' lifecycle phase -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
            <configuration>
                <!-- Show test output in the console during build -->
                <useFile>false</useFile>
                <!-- Include these naming patterns for test discovery -->
                <includes>
                    <include>**/*Test.java</include>
                    <include>**/*Tests.java</include>
                    <include>**/Test*.java</include>
                </includes>
            </configuration>
        </plugin>
    </plugins>
</build>

Step 3 β€” Maven Failsafe Plugin (Integration Tests)

For integration tests (tests that start a server, connect to a database, etc.), use the Failsafe Plugin. By convention, integration test classes end with IT:

<!-- Failsafe: runs integration tests in 'verify' phase -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>3.2.5</version>
    <executions>
        <execution>
            <goals>
                <!-- 'integration-test' runs the tests -->
                <goal>integration-test</goal>
                <!-- 'verify' checks the results and fails the build if needed -->
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <!-- Integration tests match *IT.java by convention -->
        <includes>
            <include>**/*IT.java</include>
        </includes>
    </configuration>
</plugin>

Run integration tests with: mvn verify

Continue reading JUnit 6 with Maven and Gradle: Complete Setup Guide