AgentSkillsCN

testing-junit

JUnit 5、Mockito 以及 AssertJ 用于单元测试和集成测试。涵盖测试结构、模拟、参数化测试,以及用于数据库测试的测试容器。

SKILL.md
--- frontmatter
name: testing-junit
description: JUnit 5, Mockito, and AssertJ for unit and integration testing. Covers test structure, mocking, parameterized tests, and test containers for database testing.
version: "1.0.0"
author: GazApps
tags: [junit, mockito, assertj, testing, unit-tests, integration-tests, testcontainers]
dependencies: [java-fundamentals]
compatibility: [antigravity, claude-code, gemini-cli]

Testing with JUnit 5, Mockito, and AssertJ

Comprehensive testing patterns for Java and Spring Boot applications.

Use this skill when

  • Writing unit tests for services, controllers, or utility classes
  • Mocking dependencies with Mockito
  • Writing parameterized tests for multiple input scenarios
  • Testing Spring MVC controllers with MockMvc
  • Writing integration tests with real databases using Testcontainers
  • User mentions "test", "JUnit", "Mockito", "AssertJ", "TDD", or "coverage"
  • Verifying exception handling behavior
  • Testing repository queries against a real database

Do not use this skill when

  • User wants end-to-end browser testing (use Selenium/Playwright)
  • User needs performance/load testing (use JMeter/Gatling)
  • User asks about contract testing (use Spring Cloud Contract/Pact)
  • User wants API testing only (use REST Assured standalone)
  • User needs UI component testing for frontend frameworks

Dependencies

Add to pom.xml:

xml
<!-- JUnit 5 BOM - manages all JUnit versions -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>5.11.4</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- JUnit 5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Mockito -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>5.14.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <version>5.14.2</version>
        <scope>test</scope>
    </dependency>

    <!-- AssertJ -->
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.27.0</version>
        <scope>test</scope>
    </dependency>

    <!-- Testcontainers (for integration tests) -->
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers</artifactId>
        <version>1.20.4</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>1.20.4</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>postgresql</artifactId>
        <version>1.20.4</version>
        <scope>test</scope>
    </dependency>

    <!-- Spring Boot Test (if using Spring) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<!-- Surefire plugin for running tests -->
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.5.2</version>
        </plugin>
        <!-- Failsafe plugin for integration tests (*IT.java) -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>3.5.2</version>
            <executions>
                <execution>
                    <goals>
                        <goal>integration-test</goal>
                        <goal>verify</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Note: If using Spring Boot, spring-boot-starter-test already includes JUnit 5, Mockito, and AssertJ. You only need to add Testcontainers separately.

JUnit 5 Annotations Quick Reference

AnnotationPurpose
@TestMarks a method as a test
@DisplayName("...")Human-readable test name
@BeforeEachRuns before each test method
@AfterEachRuns after each test method
@BeforeAllRuns once before all tests (must be static)
@AfterAllRuns once after all tests (must be static)
@NestedGroups related tests in an inner class
@Disabled("reason")Skips a test
@Tag("integration")Categorizes tests for filtering
@ParameterizedTestRuns test multiple times with different arguments
@RepeatedTest(n)Runs test n times
@Timeout(5)Fails test if it exceeds time limit

See references/junit5-annotations.md for the complete annotation reference with examples.

Test Lifecycle

code
@BeforeAll (once, static)
  |
  +-- @BeforeEach
  |     |-- @Test method 1
  |     +-- @AfterEach
  |
  +-- @BeforeEach
  |     |-- @Test method 2
  |     +-- @AfterEach
  |
  +-- ... more tests ...
  |
@AfterAll (once, static)

Test Structure: Arrange-Act-Assert

Every test should follow the AAA pattern:

java
@Test
@DisplayName("Should calculate total price with discount")
void shouldCalculateTotalPriceWithDiscount() {
    // Arrange - set up test data and dependencies
    var product = new Product("Laptop", new BigDecimal("1000.00"));
    var discount = new Discount(10); // 10%

    // Act - execute the behavior under test
    BigDecimal result = pricingService.calculateTotal(product, discount);

    // Assert - verify the outcome
    assertThat(result).isEqualByComparingTo(new BigDecimal("900.00"));
}

Assertions

JUnit 5 Built-in Assertions

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

assertEquals(expected, actual);
assertNotEquals(unexpected, actual);
assertTrue(condition);
assertFalse(condition);
assertNull(object);
assertNotNull(object);
assertSame(expected, actual);         // reference equality
assertArrayEquals(expected, actual);
assertIterableEquals(expected, actual);
assertThrows(Exception.class, () -> { /* code */ });
assertDoesNotThrow(() -> { /* code */ });
assertTimeout(Duration.ofSeconds(2), () -> { /* code */ });
assertAll("group",
    () -> assertEquals("John", user.getFirstName()),
    () -> assertEquals("Doe", user.getLastName())
);

AssertJ Fluent Assertions (Preferred)

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

// Strings
assertThat(name).isNotBlank()
    .startsWith("John")
    .hasSize(8)
    .containsIgnoringCase("john");

// Numbers
assertThat(age).isPositive()
    .isGreaterThan(17)
    .isBetween(18, 65);

// Collections
assertThat(users).hasSize(3)
    .extracting(User::getName)
    .containsExactly("Alice", "Bob", "Charlie");

// Objects
assertThat(actual).isEqualTo(expected);
assertThat(actual).usingRecursiveComparison()
    .ignoringFields("id", "createdAt")
    .isEqualTo(expected);

// Exceptions
assertThatThrownBy(() -> service.findById(999L))
    .isInstanceOf(ResourceNotFoundException.class)
    .hasMessageContaining("not found");

// Soft assertions (collect all failures)
SoftAssertions.assertSoftly(softly -> {
    softly.assertThat(user.getName()).isEqualTo("John");
    softly.assertThat(user.getEmail()).contains("@");
    softly.assertThat(user.getAge()).isGreaterThan(0);
});

See examples/AssertJExamples.java for comprehensive AssertJ examples.

Mockito

Basic Setup

java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;

    @InjectMocks
    private UserService userService;
}

Stubbing

java
// Return a value
when(userRepository.findById(1L)).thenReturn(Optional.of(user));

// Return different values on consecutive calls
when(service.generate()).thenReturn("first", "second", "third");

// Throw an exception
when(userRepository.findById(999L)).thenThrow(new ResourceNotFoundException("Not found"));

// Void method throw
doThrow(new RuntimeException("fail")).when(emailService).send(any());

// Answer dynamically
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
    User u = invocation.getArgument(0);
    u.setId(1L);
    return u;
});

Verification

java
// Verify method was called
verify(userRepository).save(any(User.class));

// Verify call count
verify(emailService, times(1)).send(any());
verify(emailService, never()).send(any());
verify(emailService, atLeastOnce()).send(any());

// Verify argument value
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(captor.capture());
User savedUser = captor.getValue();
assertThat(savedUser.getName()).isEqualTo("John");

// Verify call order
InOrder inOrder = inOrder(userRepository, emailService);
inOrder.verify(userRepository).save(any());
inOrder.verify(emailService).send(any());

See references/mockito-patterns.md for the complete Mockito reference. See examples/UserServiceTest.java for a full service test example. See examples/MockitoAdvancedExamples.java for advanced patterns.

Parameterized Tests

java
@ParameterizedTest
@ValueSource(strings = {"", " ", "  "})
void shouldRejectBlankNames(String name) {
    assertThatThrownBy(() -> new User(name))
        .isInstanceOf(IllegalArgumentException.class);
}

@ParameterizedTest
@CsvSource({
    "1, 1, 2",
    "5, 3, 8",
    "100, -1, 99"
})
void shouldAddNumbers(int a, int b, int expected) {
    assertThat(calculator.add(a, b)).isEqualTo(expected);
}

@ParameterizedTest
@MethodSource("validEmailProvider")
void shouldAcceptValidEmails(String email) {
    assertThat(validator.isValidEmail(email)).isTrue();
}

static Stream<String> validEmailProvider() {
    return Stream.of("user@example.com", "admin@test.org", "name+tag@domain.co");
}

@ParameterizedTest
@EnumSource(value = OrderStatus.class, names = {"PENDING", "PROCESSING"})
void shouldAllowCancellation(OrderStatus status) {
    var order = new Order(status);
    assertThat(order.canCancel()).isTrue();
}

See examples/ParameterizedExamples.java for all parameterized test types.

Exception Testing

java
// Assert exception is thrown
@Test
void shouldThrowWhenUserNotFound() {
    when(userRepository.findById(999L)).thenReturn(Optional.empty());

    assertThatThrownBy(() -> userService.findById(999L))
        .isInstanceOf(ResourceNotFoundException.class)
        .hasMessage("User not found with id: 999")
        .hasNoCause();
}

// JUnit 5 style
@Test
void shouldThrowWithJUnit() {
    var exception = assertThrows(IllegalArgumentException.class,
        () -> validator.validate(null));
    assertEquals("Input must not be null", exception.getMessage());
}

// Assert no exception
@Test
void shouldNotThrowForValidInput() {
    assertDoesNotThrow(() -> validator.validate("valid input"));
}

Testcontainers for PostgreSQL

java
@SpringBootTest
@Testcontainers
class UserRepositoryIT {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldSaveAndRetrieveUser() {
        var user = new User("John", "john@example.com");
        var saved = userRepository.save(user);

        var found = userRepository.findById(saved.getId());
        assertThat(found).isPresent()
            .get()
            .extracting(User::getName, User::getEmail)
            .containsExactly("John", "john@example.com");
    }
}

See examples/UserRepositoryIT.java for a complete integration test example.

Spring MockMvc Testing

java
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    void shouldReturnUser() throws Exception {
        when(userService.findById(1L)).thenReturn(new UserDTO(1L, "John", "john@example.com"));

        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("John"))
            .andExpect(jsonPath("$.email").value("john@example.com"));
    }
}

See examples/UserControllerTest.java for a complete controller test example.

Test Naming Conventions

Follow consistent naming for test classes and methods:

TypeConventionExample
Unit test class{ClassName}TestUserServiceTest
Integration test class{ClassName}ITUserRepositoryIT
Test methodshould{ExpectedBehavior} or should{Action}When{Condition}shouldReturnUserWhenIdExists

Test Organization with @Nested

java
@DisplayName("UserService")
class UserServiceTest {

    @Nested
    @DisplayName("findById")
    class FindById {

        @Test
        @DisplayName("should return user when exists")
        void shouldReturnUserWhenExists() { /* ... */ }

        @Test
        @DisplayName("should throw when not found")
        void shouldThrowWhenNotFound() { /* ... */ }
    }

    @Nested
    @DisplayName("createUser")
    class CreateUser {

        @Test
        @DisplayName("should save and return new user")
        void shouldSaveAndReturnNewUser() { /* ... */ }

        @Test
        @DisplayName("should throw when email already exists")
        void shouldThrowWhenEmailExists() { /* ... */ }
    }
}

Code Quality Checklist

  • Every public method in service/controller has at least one test
  • Tests follow Arrange-Act-Assert pattern
  • Tests use @DisplayName for readable test output
  • Related tests are grouped with @Nested
  • No test depends on another test's execution order
  • Mocks are verified for important interactions (verify())
  • Edge cases are covered (null, empty, boundary values)
  • Exception scenarios are tested (assertThrows, assertThatThrownBy)
  • Integration tests use Testcontainers (not H2) for database testing
  • Parameterized tests are used when testing multiple inputs
  • No production code in test scope; no test code in main scope
  • Test data is created in @BeforeEach or factory methods, not shared mutably
  • Tests run independently and can execute in any order
  • Assertions are specific (not just assertNotNull)
  • AssertJ is preferred over JUnit assertions for readability

File Reference

  • references/junit5-annotations.md - Complete JUnit 5 annotation reference
  • references/mockito-patterns.md - Complete Mockito patterns and best practices
  • examples/UserServiceTest.java - Unit test for a service with mocked repository
  • examples/UserControllerTest.java - MockMvc test for a Spring controller
  • examples/ParameterizedExamples.java - All parameterized test types
  • examples/AssertJExamples.java - AssertJ fluent assertion examples
  • examples/UserRepositoryIT.java - Integration test with Testcontainers PostgreSQL
  • examples/MockitoAdvancedExamples.java - Advanced Mockito patterns