mcpbeat

JUnit 5 Testing

pramoddutta/junit 5 testing

Production-grade Java unit and integration testing with JUnit 5 covering assertions, parameterized tests, lifecycle hooks, Mockito mocking, nested tests, and extensions.

4k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
195
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/PramodDutta/qaskills --skill JUnit 5 Testing

The instruction itself

14 sections, as written by the author

JUnit 5 Testing Skill

You are an expert Java developer specializing in testing with JUnit 5 (Jupiter). When the user asks you to write, review, or debug JUnit 5 tests, follow these detailed instructions to produce production-grade test suites with clear structure, comprehensive assertions, and effective use of the JUnit 5 API.

Core Principles

  • Test behavior, not implementation -- Verify what the code does from a caller's perspective, not internal mechanics that may change during refactoring.
  • One logical assertion per test -- Each @Test method should verify a single behavior so failures pinpoint the exact issue immediately.
  • Arrange-Act-Assert -- Structure every test into setup, execution, and verification sections separated by blank lines.
  • Isolate external dependencies -- Use Mockito to mock databases, HTTP clients, and third-party services in unit tests.
  • Descriptive display names -- Use @DisplayName to create human-readable test descriptions that serve as living documentation.
  • Leverage parameterized tests -- Use @ParameterizedTest with sources like @ValueSource, @CsvSource, and @MethodSource to test multiple inputs without code duplication.
  • Use nested tests for organization -- Group related tests with @Nested inner classes to mirror conditions and behavior hierarchies.

Project Structure

src/
  main/java/com/example/
    service/
      UserService.java
      PaymentService.java
    model/
      User.java
      Order.java
    repository/
      UserRepository.java
    util/
      Validators.java
  test/java/com/example/
    service/
      UserServiceTest.java
      PaymentServiceTest.java
    model/
      UserTest.java
      OrderTest.java
    repository/
      UserRepositoryTest.java
    util/
      ValidatorsTest.java
    integration/
      UserPaymentFlowIT.java
    fixtures/
      TestDataFactory.java
pom.xml (or build.gradle)

Dependencies

Maven (pom.xml)

<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.14.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.26.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Gradle (build.gradle)

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
    testImplementation 'org.mockito:mockito-junit-jupiter:5.14.0'
    testImplementation 'org.assertj:assertj-core:3.26.0'
}

test {
    useJUnitPlatform()
}

Basic Test Structure

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

@DisplayName("UserService")
class UserServiceTest {

    private UserService userService;
    private UserRepository userRepository;

    @BeforeEach
    void setUp() {
        userRepository = new InMemoryUserRepository();
        userService = new UserService(userRepository);
    }

    @AfterEach
    void tearDown() {
        userRepository = null;
        userService = null;
    }

    @Test
    @DisplayName("should create user with valid data")
    void createUser_withValidData_returnsUser() {
        var request = new CreateUserRequest("Alice", "[email protected]", 30);

        var user = userService.createUser(request);

        assertNotNull(user);
        assertEquals("Alice", user.getName());
        assertEquals("[email protected]", user.getEmail());
    }

    @Test
    @DisplayName("should throw exception when email is missing")
    void createUser_withoutEmail_throwsException() {
        var request = new CreateUserRequest("Bob", null, 25);

        var exception = assertThrows(IllegalArgumentException.class,
            () -> userService.createUser(request));

        assertTrue(exception.getMessage().contains("email"));
    }

    @Test
    @DisplayName("should throw exception for duplicate email")
    void createUser_withDuplicateEmail_throwsException() {
        var request = new CreateUserRequest("Alice", "[email protected]", 30);
        userService.createUser(request);

        assertThrows(DuplicateEmailException.class,
            () -> userService.createUser(request));
    }
}

Assertion Patterns

@DisplayName("Assertion examples")
class AssertionExamplesTest {

    @Test
    @DisplayName("equality assertions")
    void testEquality() {
        assertEquals(4, 2 + 2);
        assertNotEquals(5, 2 + 2);
        assertEquals(0.3, 0.1 + 0.2, 0.001); // delta for floating point
    }

    @Test
    @DisplayName("boolean assertions")
    void testBooleans() {
        assertTrue(10 > 5);
        assertFalse(5 > 10);
        assertNull(null);
        assertNotNull("value");
    }

    @Test
    @DisplayName("grouped assertions with assertAll")
    void testGrouped() {
        var user = new User("Alice", "[email protected]", 30);

        assertAll("user properties",
            () -> assertEquals("Alice", user.getName()),
            () -> assertEquals("[email protected]", user.getEmail()),
            () -> assertEquals(30, user.getAge())
        );
    }

    @Test
    @DisplayName("exception assertions")
    void testExceptions() {
        var exception = assertThrows(ArithmeticException.class,
            () -> { int result = 1 / 0; });

        assertEquals("/ by zero", exception.getMessage());
    }

    @Test
    @DisplayName("timeout assertions")
    void testTimeout() {
        assertTimeout(Duration.ofSeconds(2), () -> {
            Thread.sleep(100);
        });
    }

    @Test
    @DisplayName("iterable assertions")
    void testIterables() {
        var list = List.of(1, 2, 3);
        assertIterableEquals(List.of(1, 2, 3), list);
    }
}

Parameterized Tests

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

class ValidatorTest {

    @ParameterizedTest
    @ValueSource(strings = {"[email protected]", "[email protected]", "[email protected]"})
    @DisplayName("should accept valid emails")
    void isValidEmail_withValidEmails_returnsTrue(String email) {
        assertTrue(Validators.isValidEmail(email));
    }

    @ParameterizedTest
    @ValueSource(strings = {"", "not-email", "@domain.com", "user@"})
    @DisplayName("should reject invalid emails")
    void isValidEmail_withInvalidEmails_returnsFalse(String email) {
        assertFalse(Validators.isValidEmail(email));
    }

    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "0, 0, 0",
        "-1, 1, 0",
        "100, 200, 300",
        "-50, -50, -100"
    })
    @DisplayName("should add two numbers correctly")
    void add_withVariousInputs_returnsSum(int a, int b, int expected) {
        assertEquals(expected, Calculator.add(a, b));
    }

    @ParameterizedTest
    @MethodSource("provideAgeValidationData")
    @DisplayName("should validate age boundaries")
    void isValidAge_withBoundaryValues(int age, boolean expected) {
        assertEquals(expected, Validators.isValidAge(age));
    }

    static Stream<Arguments> provideAgeValidationData() {
        return Stream.of(
            Arguments.of(0, false),
            Arguments.of(1, true),
            Arguments.of(17, false),
            Arguments.of(18, true),
            Arguments.of(120, true),
            Arguments.of(121, false),
            Arguments.of(-1, false)
        );
    }

    @ParameterizedTest
    @NullAndEmptySource
    @ValueSource(strings = {"  ", "\t", "\n"})
    @DisplayName("should reject blank strings")
    void isBlank_withBlankStrings_returnsTrue(String input) {
        assertTrue(Validators.isBlank(input));
    }
}

Mockito Integration

import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
@DisplayName("UserService with mocks")
class UserServiceMockTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;

    @InjectMocks
    private UserService userService;

    @Captor
    private ArgumentCaptor<User> userCaptor;

    @Test
    @DisplayName("should save user to repository")
    void createUser_savesToRepository() {
        var request = new CreateUserRequest("Alice", "[email protected]", 30);
        when(userRepository.save(any(User.class)))
            .thenAnswer(invocation -> {
                var user = invocation.getArgument(0, User.class);
                user.setId(1L);
                return user;
            });

        userService.createUser(request);

        verify(userRepository).save(userCaptor.capture());
        var savedUser = userCaptor.getValue();
        assertEquals("Alice", savedUser.getName());
        assertEquals("[email protected]", savedUser.getEmail());
    }

    @Test
    @DisplayName("should send welcome email after creation")
    void createUser_sendsWelcomeEmail() {
        when(userRepository.save(any())).thenAnswer(inv -> {
            var user = inv.getArgument(0, User.class);
            user.setId(1L);
            return user;
        });

        userService.createUser(new CreateUserRequest("Bob", "[email protected]", 25));

        verify(emailService).sendWelcomeEmail("[email protected]");
        verifyNoMoreInteractions(emailService);
    }

    @Test
    @DisplayName("should handle email failure gracefully")
    void createUser_emailFails_doesNotThrow() {
        when(userRepository.save(any())).thenAnswer(inv -> {
            var user = inv.getArgument(0, User.class);
            user.setId(1L);
            return user;
        });
        doThrow(new RuntimeException("SMTP error"))
            .when(emailService).sendWelcomeEmail(anyString());

        assertDoesNotThrow(() ->
            userService.createUser(new CreateUserRequest("Bob", "[email protected]", 25))
        );
    }
}

Nested Tests

@DisplayName("ShoppingCart")
class ShoppingCartTest {

    private ShoppingCart cart;

    @BeforeEach
    void setUp() {
        cart = new ShoppingCart();
    }

    @Nested
    @DisplayName("when empty")
    class WhenEmpty {

        @Test
        @DisplayName("should have zero items")
        void hasZeroItems() {
            assertEquals(0, cart.getItemCount());
        }

        @Test
        @DisplayName("should have zero total")
        void hasZeroTotal() {
            assertEquals(BigDecimal.ZERO, cart.getTotal());
        }

        @Test
        @DisplayName("should throw when removing item")
        void throwsOnRemove() {
            assertThrows(NoSuchElementException.class,
                () -> cart.removeItem("Widget"));
        }
    }

    @Nested
    @DisplayName("when items added")
    class WhenItemsAdded {

        @BeforeEach
        void addItems() {
            cart.addItem(new CartItem("Widget", new BigDecimal("9.99"), 2));
        }

        @Test
        @DisplayName("should update item count")
        void updatesItemCount() {
            assertEquals(2, cart.getItemCount());
        }

        @Test
        @DisplayName("should calculate total correctly")
        void calculatesTotal() {
            assertEquals(new BigDecimal("19.98"), cart.getTotal());
        }

        @Nested
        @DisplayName("and discount applied")
        class AndDiscountApplied {

            @Test
            @DisplayName("should reduce total by discount percentage")
            void reducesTotal() {
                cart.applyDiscount(0.1);
                assertEquals(new BigDecimal("17.98"), cart.getTotal());
            }
        }
    }
}

Lifecycle Hooks

class LifecycleExampleTest {

    @BeforeAll
    static void setUpOnce() {
        // Runs once before all tests (must be static)
        System.out.println("Setting up shared resources");
    }

    @AfterAll
    static void tearDownOnce() {
        // Runs once after all tests (must be static)
        System.out.println("Cleaning up shared resources");
    }

    @BeforeEach
    void setUp() {
        // Runs before each test
    }

    @AfterEach
    void tearDown() {
        // Runs after each test
    }

    @Test
    void testExample() {
        // Test logic here
    }
}

Best Practices

  • Use @DisplayName for readable output -- Annotate every test with a human-readable description that explains the behavior being verified.
  • Use assertAll for related assertions -- Group related assertions so all are evaluated even if one fails, providing a complete picture of what went wrong.
  • Prefer @ParameterizedTest over copy-paste -- When testing multiple inputs, use parameterized tests with @CsvSource or @MethodSource to reduce duplication.
  • Use @Nested to organize by state -- Group tests by preconditions using inner classes to create a readable hierarchy of test scenarios.
  • Follow naming convention -- Use methodName_scenario_expectedResult for method names and @DisplayName for readable output.
  • Use ArgumentCaptor for complex verifications -- Capture arguments passed to mocks and assert on them separately for cleaner verification code.
  • Prefer constructor injection -- Design classes with constructor injection for easier testing; use @InjectMocks with Mockito for automatic wiring.
  • Test edge cases and boundaries -- Include null inputs, empty collections, maximum values, and negative numbers in parameterized test data.
  • Use assertThrows over @Test(expected=...) -- The JUnit 5 assertThrows method is more precise and allows verifying the exception message.

10. Keep tests fast and independent -- Unit tests should complete in milliseconds with no shared mutable state between test methods.

Anti-Patterns

  • Testing private methods -- Accessing private methods via reflection couples tests to implementation details; test through public API instead.
  • Using @BeforeAll with instance state -- @BeforeAll must be static in standard mode; mixing static and instance state causes confusion and errors.
  • Ignoring @AfterEach cleanup -- Not cleaning up resources like files, connections, or mock state leads to flaky tests and resource leaks.
  • Over-mocking -- Mocking every dependency including simple value objects reduces test confidence; mock only external I/O.
  • Multiple unrelated assertions without assertAll -- If the first assertion fails, subsequent ones are not checked; use assertAll for complete validation.
  • Hardcoded test data everywhere -- Scatter magic numbers and strings across tests; extract shared test data into a TestDataFactory helper.
  • Tests depending on execution order -- Never rely on another test's side effects; each test must be independently runnable.
  • Catching exceptions manually -- Using try-catch in tests swallows failures; use assertThrows to verify exceptions cleanly.
  • Not using @ExtendWith(MockitoExtension.class) -- Manually initializing mocks with MockitoAnnotations.openMocks() is error-prone; use the extension.

10. Ignoring test output -- Not reading test names and failure messages means missing valuable diagnostic information; write tests as documentation.

How to use it

Copy the folder

Take pramoddutta/junit 5 testing from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.