Junit 5 Quick Guide

JUnit 5 is the modern testing framework for Java. It helps you write unit tests, parameterized tests, and integration-friendly test suites with clean annotations and powerful extensions.

In this guide, you’ll learn JUnit 5 from scratch with practical examples, best practices, and real-world patterns.

What is JUnit 5?

JUnit 5 is the latest major version of JUnit. It is made of three parts:

  • JUnit Platform: Launches tests on the JVM (IDE, Maven, Gradle).
  • JUnit Jupiter: The new programming model + annotations (what you use most).
  • JUnit Vintage: Runs older JUnit 3/4 tests (optional).

Keyword targets: JUnit 5 tutorial, JUnit Jupiter, JUnit 5 annotations, Java unit testing.

Why JUnit 5 is Better Than JUnit 4

  • New annotations: @BeforeEach, @AfterEach, @BeforeAll, @AfterAll
  • Better assertions: assertAll, assertTimeout, lambda-based assertThrows
  • Parameterized tests built-in
  • Tags + nested tests + repeated tests
  • Extension model replaces old runners (@ExtendWith)

JUnit 5 Setup (Maven and Gradle)

Maven (JUnit 5 dependency + Surefire)

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

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.2.5</version>
      <configuration>
        <useModulePath>false</useModulePath>
      </configuration>
    </plugin>
  </plugins>
</build>

Gradle (JUnit Platform)

dependencies {
  testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}

test {
  useJUnitPlatform()
}

Run tests:

  • Maven: mvn test
  • Gradle: ./gradlew test

JUnit 5 Test Class Basics

A JUnit 5 test method uses @Test.

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

class CalculatorTest {

  @Test
  void addsTwoNumbers() {
    int sum = 10 + 20;
    assertEquals(30, sum);
  }
}

Use method names like shouldAddTwoNumbers() or addsTwoNumbers().


JUnit 5 Lifecycle Annotations

Lifecycle methods help you set up and clean up test state.

import org.junit.jupiter.api.*;

class LifecycleTest {

  @BeforeAll
  static void beforeAll() { /* runs once before all tests */ }

  @AfterAll
  static void afterAll() { /* runs once after all tests */ }

  @BeforeEach
  void beforeEach() { /* runs before each test */ }

  @AfterEach
  void afterEach() { /* runs after each test */ }

  @Test
  void sampleTest() {
    assertTrue(true);
  }
}

Note: @BeforeAll and @AfterAll must be static unless you use @TestInstance(Lifecycle.PER_CLASS).


JUnit 5 Assertions (Most Used)

Basic assertions

assertEquals(5, 2 + 3);
assertNotEquals(5, 2 + 2);
assertTrue(10 > 1);
assertFalse(1 > 10);
assertNull(null);
assertNotNull("hello");

Assert with message

assertEquals(10, value, "Value should be 10");

Grouped assertions with assertAll (great for DTO validation)

assertAll("user",
  () -> assertEquals("Susil", user.getName()),
  () -> assertEquals(40, user.getAge()),
  () -> assertTrue(user.isActive())
);

Assert exceptions with assertThrows

var ex = assertThrows(IllegalArgumentException.class,
  () -> Integer.parseInt("abc")
);
assertTrue(ex.getMessage() == null || ex.getMessage().length() >= 0);

Timeouts

assertTimeout(java.time.Duration.ofMillis(200), () -> {
  Thread.sleep(50);
});

JUnit 5 Assumptions (Skip Tests Conditionally)

Assumptions abort a test when conditions don’t match (useful for OS/env-specific tests).

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

@Test
void runsOnlyOnCi() {
  assumeTrue("true".equals(System.getenv("CI")));
  // test logic runs only in CI
}

Parameterized Tests in JUnit 5 (Essential for Real Projects)

Add dependency (if not using junit-jupiter BOM style):

  • org.junit.jupiter:junit-jupiter-params

@ValueSource

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;

class StringTest {

  @ParameterizedTest
  @ValueSource(strings = {"racecar", "madam", "level"})
  void shouldBePalindrome(String word) {
    assertTrue(new StringBuilder(word).reverse().toString().equals(word));
  }
}

@CsvSource (best for business rules)

import org.junit.jupiter.params.provider.CsvSource;

@ParameterizedTest
@CsvSource({
  "10, 20, 30",
  "5,  7,  12"
})
void addsUsingCsv(int a, int b, int expected) {
  assertEquals(expected, a + b);
}

@MethodSource (complex data)

import java.util.stream.Stream;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

static Stream<Arguments> discountCases() {
  return Stream.of(
    Arguments.of(1000, 0.10, 900),
    Arguments.of(200,  0.05, 190)
  );
}

@ParameterizedTest
@MethodSource("discountCases")
void calculatesDiscount(int price, double rate, int expected) {
  int finalPrice = (int)(price * (1 - rate));
  assertEquals(expected, finalPrice);
}

Display Names, Ordering, and Tags

@DisplayName

@Test
@DisplayName("Should calculate GST correctly")
void gstTest() { }

@Tag (run subsets of tests)

import org.junit.jupiter.api.Tag;

@Test
@Tag("fast")
void fastTest() { }

Maven example:

  • Run only fast tests: mvn test -Dgroups=fast (tag filtering differs by setup; often done via Surefire configuration)

Nested Tests (Organize Like a Spec)

import org.junit.jupiter.api.*;

class AuthServiceTest {

  @Nested
  class Login {

    @Test
    void shouldFailForWrongPassword() { }

    @Test
    void shouldPassForCorrectPassword() { }
  }

  @Nested
  class Signup {
    @Test
    void shouldRejectWeakPassword() { }
  }
}

This structure reads like documentation and improves blog clarity too.


Repeated Tests

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

class RetryLikeTest {

  @RepeatedTest(3)
  void repeated() {
    assertTrue(2 + 2 == 4);
  }
}

Disable Tests (with reason)

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

@Disabled("Waiting for bug fix in payment gateway sandbox")
@Test
void paymentSandboxTest() { }

Test Instance Lifecycle (PER_CLASS vs PER_METHOD)

Default is PER_METHOD (new instance per test). For expensive setup, sometimes you use PER_CLASS.

import org.junit.jupiter.api.TestInstance;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ExpensiveSetupTest {
  // @BeforeAll can be non-static now
}

Use carefully: shared state can cause flaky tests.


JUnit 5 Best Practices (Real-World)

1) Follow AAA pattern

  • Arrange: setup data
  • Act: call method
  • Assert: verify output

2) One logical behavior per test

Avoid giant tests that validate 20 things.

3) Use descriptive names

Bad: test1()
Good: shouldThrowExceptionWhenUserNotFound()

4) Prefer parameterized tests for rule matrices

You’ll reduce duplicates dramatically.

5) Keep tests independent

No test should depend on execution order.


Common Pitfalls (and Fixes)

  • Tests not running: Ensure Gradle uses useJUnitPlatform() and Maven Surefire is updated.
  • @BeforeAll not static: either make it static or add @TestInstance(PER_CLASS).
  • Flaky tests: remove shared mutable state, avoid real time sleeps, isolate random seeds.

JUnit 5 FAQ

Is JUnit 5 backward compatible with JUnit 4?

Yes, via JUnit Vintage, but prefer migrating to Jupiter for new code.

What is the difference between JUnit Jupiter and JUnit Platform?

Platform runs tests; Jupiter provides annotations + APIs for writing tests.

Should I use Mockito with JUnit 5?

Yes, commonly. Mockito has a JUnit 5 integration via extensions.

What is the best way to name unit tests?

Use behavior-driven names: shouldDoXWhenY().


Conclusion

JUnit 5 makes Java testing cleaner with modern annotations, better assertions, and first-class parameterized testing. If you adopt good naming + structure (nested tests, tags, AAA), your test suite becomes readable documentation.