← All skills

JUnit 5 Skill

Unit testingJava

Copy and Paste in your Terminal

npx skills add https://github.com/LambdaTest/agent-skills.git --skill junit-5-skill

Playbook

Complete implementation guide with code samples, patterns, and best practices.

JUnit 5 — Advanced Implementation Playbook

§1 — Project Setup & Configuration

<!-- pom.xml -->
<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.2</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.25.3</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>
    </plugin>
  </plugins>
</build>
# src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=concurrent
junit.jupiter.execution.parallel.config.fixed.parallelism=4
junit.jupiter.testinstance.lifecycle.default=per_class

§2 — Test Lifecycle & Structure

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayName("User Service Tests")
class UserServiceTest {
    private UserService service;
    private UserRepository mockRepo;

    @BeforeAll
    static void initAll() {
        // Run once — DB connection, heavy resources
    }

    @BeforeEach
    void setUp() {
        mockRepo = Mockito.mock(UserRepository.class);
        service = new UserService(mockRepo);
    }

    @Test
    @DisplayName("Creates user with valid data")
    @Tag("smoke")
    void testCreateUser() {
        User user = new User("Alice", "alice@test.com");
        when(mockRepo.save(any(User.class))).thenReturn(user);
        User result = service.create("Alice", "alice@test.com");
        assertAll(
            () -> assertEquals("Alice", result.getName()),
            () -> assertEquals("alice@test.com", result.getEmail()),
            () -> verify(mockRepo).save(any(User.class))
        );
    }

    @AfterEach
    void tearDown() {
        Mockito.reset(mockRepo);
    }

    @AfterAll
    static void tearDownAll() {
        // Cleanup heavy resources
    }
}

§3 — Parameterized Tests

// CSV Source
@ParameterizedTest(name = "validate({0}) = {1}")
@CsvSource({
    "user@test.com, true",
    "invalid, false",
    "'', false",
    "user@.com, false",
})
void testEmailValidation(String email, boolean expected) {
    assertEquals(expected, EmailValidator.isValid(email));
}

// CSV File Source
@ParameterizedTest
@CsvFileSource(resources = "/testdata/login-scenarios.csv", numLinesToSkip = 1)
void testLoginScenarios(String email, String password, String expectedResult) {
    LoginResult result = authService.login(email, password);
    assertEquals(expectedResult, result.getStatus());
}

// Method Source — complex objects
@ParameterizedTest
@MethodSource("provideUsers")
void testUserCreation(String name, String email, boolean shouldSucceed) {
    if (shouldSucceed) {
        assertDoesNotThrow(() -> service.create(name, email));
    } else {
        assertThrows(ValidationException.class, () -> service.create(name, email));
    }
}

static Stream<Arguments> provideUsers() {
    return Stream.of(
        Arguments.of("Alice", "alice@test.com", true),
        Arguments.of("", "bob@test.com", false),
        Arguments.of("Charlie", "", false)
    );
}

// Enum Source
@ParameterizedTest
@EnumSource(value = UserRole.class, names = {"ADMIN", "EDITOR"})
void testWritePermissions(UserRole role) {
    assertTrue(PermissionService.canWrite(role));
}

// Value Source
@ParameterizedTest
@ValueSource(strings = {"admin", "editor", "viewer"})
void testRoleExists(String role) {
    assertNotNull(RoleService.findByName(role));
}

§4 — Mockito Integration

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock private OrderRepository orderRepo;
    @Mock private PaymentGateway paymentGateway;
    @Mock private EmailService emailService;
    @InjectMocks private OrderService orderService;

    @Test
    void testPlaceOrder() {
        Order order = new Order("item-1", 29.99);
        when(paymentGateway.charge(anyDouble())).thenReturn(true);
        when(orderRepo.save(any(Order.class))).thenReturn(order);

        Order result = orderService.placeOrder(order);

        assertNotNull(result);
        verify(paymentGateway).charge(29.99);
        verify(orderRepo).save(order);
        verify(emailService).sendConfirmation(eq(order), anyString());
    }

    @Test
    void testPlaceOrder_paymentFails() {
        when(paymentGateway.charge(anyDouble())).thenReturn(false);
        assertThrows(PaymentException.class, () ->
            orderService.placeOrder(new Order("item-1", 29.99))
        );
        verify(orderRepo, never()).save(any());
    }

    // Argument captor
    @Captor private ArgumentCaptor<Order> orderCaptor;

    @Test
    void testOrderCapture() {
        when(paymentGateway.charge(anyDouble())).thenReturn(true);
        orderService.placeOrder(new Order("item-1", 29.99));
        verify(orderRepo).save(orderCaptor.capture());
        assertEquals("item-1", orderCaptor.getValue().getItemId());
    }

    // Verify call order
    @Test
    void testCallOrder() {
        when(paymentGateway.charge(anyDouble())).thenReturn(true);
        orderService.placeOrder(new Order("item-1", 29.99));
        InOrder inOrder = inOrder(paymentGateway, orderRepo, emailService);
        inOrder.verify(paymentGateway).charge(anyDouble());
        inOrder.verify(orderRepo).save(any());
        inOrder.verify(emailService).sendConfirmation(any(), anyString());
    }
}

§5 — Nested & Dynamic Tests

@DisplayName("Calculator")
class CalculatorTest {
    Calculator calc = new Calculator();

    @Nested
    @DisplayName("Addition")
    class AdditionTests {
        @Test void positiveNumbers() { assertEquals(5, calc.add(2, 3)); }
        @Test void negativeNumbers() { assertEquals(-5, calc.add(-2, -3)); }
        @Test void zero() { assertEquals(3, calc.add(3, 0)); }
    }

    @Nested
    @DisplayName("Division")
    class DivisionTests {
        @Test void normalDivision() { assertEquals(2, calc.divide(6, 3)); }
        @Test void divisionByZero() {
            assertThrows(ArithmeticException.class, () -> calc.divide(6, 0));
        }
    }

    // Dynamic tests generated at runtime
    @TestFactory
    Stream<DynamicTest> testSquareRoots() {
        return Stream.of(
            new int[]{4, 2}, new int[]{9, 3}, new int[]{16, 4}, new int[]{25, 5}
        ).map(pair -> DynamicTest.dynamicTest(
            "sqrt(" + pair[0] + ") = " + pair[1],
            () -> assertEquals(pair[1], (int) Math.sqrt(pair[0]))
        ));
    }
}

§6 — AssertJ Fluent Assertions

import static org.assertj.core.api.Assertions.*;

@Test
void testWithAssertJ() {
    User user = userService.findById(1);

    assertThat(user)
        .isNotNull()
        .extracting(User::getName, User::getEmail)
        .containsExactly("Alice", "alice@test.com");

    assertThat(user.getRoles())
        .hasSize(2)
        .contains("ADMIN")
        .doesNotContain("VIEWER");

    assertThat(user.getCreatedAt())
        .isAfter(LocalDate.of(2024, 1, 1))
        .isBefore(LocalDate.now());

    // Exception assertion
    assertThatThrownBy(() -> userService.findById(999))
        .isInstanceOf(NotFoundException.class)
        .hasMessageContaining("not found");

    // Collection assertions
    List<User> users = userService.findAll();
    assertThat(users)
        .hasSizeGreaterThan(0)
        .extracting(User::getName)
        .contains("Alice", "Bob")
        .doesNotContain("Unknown");
}

§7 — Conditional Execution & Assumptions

@Test
@EnabledOnOs(OS.LINUX)
void testLinuxOnly() { /* ... */ }

@Test
@DisabledIfEnvironmentVariable(named = "CI", matches = "true")
void testLocalOnly() { /* ... */ }

@Test
@EnabledIf("isDevEnvironment")
void testDevOnly() { /* ... */ }

static boolean isDevEnvironment() {
    return "dev".equals(System.getProperty("env"));
}

@Test
void testWithAssumption() {
    assumeTrue(System.getenv("API_KEY") != null, "API key required");
    // Test only runs if API key is set
}

§8 — Custom Extensions

// Timing extension — log test duration
public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
    @Override
    public void beforeTestExecution(ExtensionContext ctx) {
        ctx.getStore(Namespace.GLOBAL).put("start", System.currentTimeMillis());
    }

    @Override
    public void afterTestExecution(ExtensionContext ctx) {
        long start = ctx.getStore(Namespace.GLOBAL).get("start", Long.class);
        long duration = System.currentTimeMillis() - start;
        System.out.printf("⏱ %s: %dms%n", ctx.getDisplayName(), duration);
    }
}
// Usage: @ExtendWith(TimingExtension.class)

// Retry extension — retry flaky tests
public class RetryExtension implements TestExecutionExceptionHandler {
    @Override
    public void handleTestExecutionException(ExtensionContext ctx, Throwable t) throws Throwable {
        int maxRetries = 2;
        int retries = ctx.getStore(Namespace.GLOBAL).getOrDefault("retry", Integer.class, 0);
        if (retries < maxRetries) {
            ctx.getStore(Namespace.GLOBAL).put("retry", retries + 1);
        } else {
            throw t;
        }
    }
}

§9 — CI/CD Integration

name: JUnit Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: 17 }
      - name: Cache Maven
        uses: actions/cache@v4
        with:
          path: ~/.m2/repository
          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
      - name: Run tests
        run: mvn test -Dparallel=true
      - name: Publish test results
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: JUnit Results
          path: target/surefire-reports/TEST-*.xml
          reporter: java-junit

§10 — Debugging Quick-Reference

ProblemCauseFix
Test not discoveredWrong naming or missing @TestName *Test.java, add @Test annotation
@BeforeAll must be staticInstance lifecycle is per_methodUse @TestInstance(Lifecycle.PER_CLASS) or make static
Mock returns nullMissing when().thenReturn()Add stubbing or use @Mock with lenient()
Parameterized failsWrong arg count or typesEnsure source matches parameter list
Parallel tests flakyShared mutable stateUse @Isolated or separate state per test
Extension not appliedMissing @ExtendWithAdd annotation or register in META-INF/services
assertAll shows one errorExpected — runs all, reports firstUse assertAll to see all failures at once
Slow test suiteNo parallelismEnable in junit-platform.properties

§11 — Best Practices Checklist

  • ✅ Use @DisplayName for readable test names
  • ✅ Use assertAll() to check multiple conditions together
  • ✅ Use @ParameterizedTest over copy-pasting test methods
  • ✅ Use @ExtendWith(MockitoExtension.class) for Mockito
  • ✅ Use @Nested for logical grouping of related tests
  • ✅ Use @Tag for categorizing tests (smoke, integration, etc.)
  • ✅ Use AssertJ for fluent, readable assertions
  • ✅ Use ArgumentCaptor for verifying complex arguments
  • ✅ Use @TestFactory for dynamic test generation
  • ✅ Enable parallel execution in CI for speed
  • ✅ One logical assertion per test (use assertAll for compound checks)
  • ✅ Structure: src/test/java/ mirroring src/main/java/