# Testing Junit

> JUnit 5, Mockito, and AssertJ for unit and integration testing. Covers test structure, mocking, parameterized tests, and test containers for database testing.

- Skill: `gazolla/testing-junit` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add gazolla/testing-junit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gazolla/testing-junit/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: gazolla (https://skillmd.com/u/gazolla)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/gazolla/testing-junit

---


# 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

| Annotation | Purpose |
|---|---|
| `@Test` | Marks a method as a test |
| `@DisplayName("...")` | Human-readable test name |
| `@BeforeEach` | Runs before each test method |
| `@AfterEach` | Runs after each test method |
| `@BeforeAll` | Runs once before all tests (must be `static`) |
| `@AfterAll` | Runs once after all tests (must be `static`) |
| `@Nested` | Groups related tests in an inner class |
| `@Disabled("reason")` | Skips a test |
| `@Tag("integration")` | Categorizes tests for filtering |
| `@ParameterizedTest` | Runs 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

```
@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:

| Type | Convention | Example |
|---|---|---|
| Unit test class | `{ClassName}Test` | `UserServiceTest` |
| Integration test class | `{ClassName}IT` | `UserRepositoryIT` |
| Test method | `should{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

