Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Mastering JUnit 5: Annotations, Lifecycle, and Test Suites

Tech 1

JUnit 5 Architecture

JUnit 5 represents a major evolution in the JUnit framework, designed for Java 8 and above. It is composed of three distinct modules: JUnit Platform, JUnit Jupiter, and JUnit Vintage.

  • JUnit Platform: Serves as the foundation for launching testing frameworks on the JVM. It provides a stable API for tools like IDEs and build tools (Maven, Gradle) to discover and execute tests.
  • JUnit Jupiter: The core module combining the new programming model and extension model. It contains the annotations and APIs used to write modern JUnit 5 tests.
  • JUnit Vintage: Provides backward compatibility, allowing the execution of tests written in JUnit 3 and JUnit 4 within the JUnit 5 environment.

Dependency Configuration

To utilize JUnit 5 in a Maven project, the essential dependencies include the Jupiter API for writing tests, the engine for running them, and the params dependency for parameterized tests.

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.10.0</version>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-params</artifactId>
        <version>5.10.0</version>
    </dependency>
</dependencies>

Core Annotations and Lifecycle

The framework provides a robust set of annotations to control the test execution lifecycle.

Basic Test Methods

The @Test annotation marks a method as a test case. Unlike previous versions, it does not require public visibility.

public class BasicTest {
    @Test
    void verifyAddition() {
        System.out.println("Executing addition test");
    }

    @Test
    void verifySubtraction() {
        System.out.println("Executing subtraction test");
    }
}

Setup and Teardown

JUnit 5 offers fine-grained control over initialization and cleanup:

  • @BeforeAll: Executed once before all tests in the class. Must be static.
  • @AfterAll: Executed once after all tests in the class. Must be static.
  • @BeforeEach: Executed before each individual test method.
  • @AfterEach: Executed after each individual test method.
public class LifecycleTest {
    @BeforeAll
    static void setupGlobal() {
        System.out.println("Global initialization");
    }

    @AfterAll
    static void teardownGlobal() {
        System.out.println("Global cleanup");
    }

    @BeforeEach
    void setupTest() {
        System.out.println("Initializing test data");
    }

    @AfterEach
    void cleanupTest() {
        System.out.println("Cleaning test data");
    }

    @Test
    void testCaseOne() {
        System.out.println("Test Case 1 logic");
    }
}

Disabling Tests

The @Disabled annotation can be applied to a class or method to prevent execution.

public class SkipTest {
    @Test
    @Disabled("Functionality under review")
    void incompleteTest() {
        System.out.println("This will not run");
    }
}

Parameterized Tests

Parameterized tests allow a single test method to run multiple times with different arguments.

Using ValueSource

@ValueSource allows passing a simple array of literals (strings, ints, etc.).

public class ParamTest {
    @ParameterizedTest
    @ValueSource(strings = {"Apple", "Banana", "Cherry"})
    void checkFruitNames(String fruit) {
        System.out.println("Validating fruit: " + fruit);
        assertNotNull(fruit);
    }
}

Using CsvSource

@CsvSource allows for multiple arguments to be passed as comma-separated values.

public class CsvParamTest {
    @ParameterizedTest
    @CsvSource({"10, 20, 30", "5, 15, 20"})
    void checkSum(int num1, int num2, int expectedSum) {
        assertEquals(expectedSum, num1 + num2);
    }
}

Using MethodSource

For complex data types, @MethodSource references a static factory method that returns a Stream of Arguments.

public class MethodParamTest {
    @ParameterizedTest
    @MethodSource("userDataProvider")
    void testUserCreation(String name, int age) {
        System.out.println("User: " + name + ", Age: " + age);
    }

    static Stream<Arguments> userDataProvider() {
        return Stream.of(
            Arguments.arguments("Alice", 25),
            Arguments.arguments("Bob", 30)
        );
    }
}

Test Execution Order

By default, JUnit 5 does not guarantee the order of test method execution. To enforce a specific sequence, use @TestMethodOrder in conjunction with @Order.

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OrderedTest {
    @Test
    @Order(2)
    void secondStep() {
        System.out.println("Step 2");
    }

    @Test
    @Order(1)
    void firstStep() {
        System.out.println("Step 1");
    }
}

Test Instance Lifecycle

The default behavior creates a new instance of the test class for each test method (PER_METHOD). This ensures test isolation. To share state between tests, switch to PER_CLASS lifecycle using @TestInstance.

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class SharedStateTest {
    int sharedCounter = 0;

    @Test
    @Order(1)
    void incrementOnce() {
        sharedCounter++;
        System.out.println("Counter: " + sharedCounter); // Output: 1
    }

    @Test
    @Order(2)
    void incrementAgain() {
        sharedCounter++;
        System.out.println("Counter: " + sharedCounter); // Output: 2
    }
}

Assertions

Assertions are the core mechanism to verify expected outcomes. JUnit 5 provides static methods in the org.junit.jupiter.api.Assertions class.

MethodDescription
assertEquals(expected, actual)Verifies that two values are equal.
assertTrue(condition)Verifies that the condition is true.
assertNotNull(object)Verifies that the object is not null.
assertTimeout(duration, executable)Verifies that execution completes within the given time.
public class AssertionTest {
    @Test
    void validateMath() {
        int sum = 5 + 5;
        assertEquals(10, sum);
        assertTrue(sum > 0);
    }

    @Test
    void checkTimeout() {
        assertTimeout(Duration.ofSeconds(2), () -> {
            Thread.sleep(1000);
        });
    }
}

Test Suites

Test suites allow grouping multiple test classes for simultaneous execution. This requires the junit-platform-suite dependency.

@Suite
@SelectClasses({BasicTest.class, LifecycleTest.class})
public class RegressionSuite {
    // This class acts as a container for the suite
}

Alternatively, packages can be selected using @SelectPackages.

@Suite
@SelectPackages("com.example.tests")
public class PackageSuite {
}

When selecting packages, JUnit 5 defaults to scanning for classes following specific naming conventions: ending in Test, starting with Test, ending in IT, or ending in ITCase.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.