← 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

Advanced patterns

Advanced topics and patterns for experienced users.

JUnit 5 — Advanced Patterns & Playbook

Parameterized Tests

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

@ParameterizedTest(name = "{0} is valid email: {1}")
@CsvSource({"alice@test.com, true", "invalid, false", "@bad.com, false"})
void testEmailValidation(String email, boolean expected) {
    assertEquals(expected, validator.isValidEmail(email));
}

@ParameterizedTest
@MethodSource("userProvider")
void testUserCreation(String name, int age, String role) {
    User user = new User(name, age, role);
    assertAll(
        () -> assertEquals(name, user.getName()),
        () -> assertEquals(age, user.getAge()),
        () -> assertEquals(role, user.getRole())
    );
}

static Stream<Arguments> userProvider() {
    return Stream.of(
        Arguments.of("Alice", 30, "ADMIN"),
        Arguments.of("Bob", 25, "USER")
    );
}

// Custom argument provider
@ParameterizedTest
@ArgumentsSource(RandomUsersProvider.class)
void testWithRandomData(User user) { assertNotNull(user.getId()); }

static class RandomUsersProvider implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext ctx) {
        return Stream.generate(() -> Arguments.of(User.random())).limit(10);
    }
}

Extension Model

// Retry extension
public class RetryExtension implements TestExecutionExceptionHandler {
    @Override
    public void handleTestExecutionException(ExtensionContext ctx, Throwable t) throws Throwable {
        int maxRetries = ctx.getTestMethod()
            .map(m -> m.getAnnotation(Retry.class))
            .map(Retry::value).orElse(0);
        for (int i = 0; i < maxRetries; i++) {
            try {
                ctx.getTestMethod().get().invoke(ctx.getTestInstance().get());
                return;
            } catch (Throwable retry) { /* continue */ }
        }
        throw t;
    }
}

// Database extension with transaction rollback
public class DatabaseExtension implements BeforeEachCallback, AfterEachCallback {
    @Override
    public void beforeEach(ExtensionContext ctx) {
        getStore(ctx).put("tx", DataSource.beginTransaction());
    }
    @Override
    public void afterEach(ExtensionContext ctx) {
        getStore(ctx).get("tx", Transaction.class).rollback();
    }
    private ExtensionContext.Store getStore(ExtensionContext ctx) {
        return ctx.getStore(ExtensionContext.Namespace.create(getClass(), ctx.getRequiredTestMethod()));
    }
}

// Usage
@ExtendWith({DatabaseExtension.class, RetryExtension.class})
class UserServiceTest { /* ... */ }

Nested & Dynamic Tests

@Nested
@DisplayName("When user is authenticated")
class AuthenticatedUser {
    @BeforeEach
    void login() { session = authService.login("admin", "pass"); }

    @Test
    void canAccessDashboard() { assertTrue(session.canAccess("/dashboard")); }

    @Nested
    @DisplayName("with admin role")
    class AdminRole {
        @Test
        void canDeleteUsers() { assertTrue(session.canDelete("/users/1")); }
    }
}

// Dynamic tests generated at runtime
@TestFactory
Stream<DynamicTest> dynamicTestsFromFiles() {
    return Files.list(Path.of("test-data/"))
        .filter(p -> p.toString().endsWith(".json"))
        .map(path -> DynamicTest.dynamicTest(
            "Test: " + path.getFileName(),
            () -> { TestCase tc = loadTestCase(path); assertEquals(tc.expected, process(tc.input)); }
        ));
}

Mockito Integration

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock UserRepository userRepo;
    @Mock PaymentGateway gateway;
    @Spy NotificationService notifier;
    @InjectMocks OrderService orderService;
    @Captor ArgumentCaptor<Payment> paymentCaptor;

    @Test
    void processOrder() {
        when(userRepo.findById(1L)).thenReturn(Optional.of(new User(1L, "Alice")));
        when(gateway.charge(any())).thenReturn(PaymentResult.success("txn-123"));

        orderService.process(new Order(1L, BigDecimal.TEN));

        verify(gateway).charge(paymentCaptor.capture());
        assertEquals(BigDecimal.TEN, paymentCaptor.getValue().getAmount());
        verify(notifier).send(argThat(msg -> msg.contains("txn-123")));
    }

    @Test
    void handlesPaymentFailure() {
        when(userRepo.findById(anyLong())).thenReturn(Optional.of(new User()));
        when(gateway.charge(any())).thenReturn(PaymentResult.failure("declined"));

        assertThrows(PaymentException.class, () -> orderService.process(new Order(1L, BigDecimal.TEN)));
        verify(notifier, never()).send(anyString());
    }
}

Configuration

<!-- pom.xml — production-grade -->
<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.11.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <version>5.12.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>
<build><plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.3.0</version>
        <configuration>
            <parallel>methods</parallel>
            <threadCount>4</threadCount>
            <includes><include>**/*Test.java</include></includes>
        </configuration>
    </plugin>
</plugins></build>

Anti-Patterns

  • @Test void test1() — use descriptive names: shouldThrowWhenInputNull
  • ❌ Multiple assertions without assertAll — first failure hides subsequent issues
  • @BeforeAll non-static in non-@TestInstance(PER_CLASS) — causes runtime error
  • ❌ Mocking value objects — mock interfaces and services, not POJOs
  • assertEquals(true, result) → use assertTrue(result)