Claude
Skills
Sign in
Back

java-unit-tests

Included with Lifetime
$97 forever

Comprehensive guidance for writing high-quality unit tests in Java projects using JUnit 5 and AssertJ. Use when writing unit tests, creating test classes, or need guidance on mocking strategies, assertions, test builders, or JUnit 5 best practices. Requires JUnit 5, AssertJ, and Mockito dependencies.

Backend & APIs

What this skill does


# Java Unit Testing Skill

## Overview
This skill provides comprehensive guidance for writing high-quality unit tests in Java projects using JUnit 5 and AssertJ. It enforces best practices for test structure, mocking strategy, and assertion patterns.

## Core Principles

### Test Structure
- Use `lower_snake_case` for test method names
- Annotate test phases with `// given`, `// when`, `// then` comments
- One logical assertion per test (multiple chained AssertJ assertions are acceptable)
- Tests should be self-contained and independent
- Use `@BeforeEach` sparingly - prefer test-scoped setup in the given phase
- Inline simple construction as `private final` fields when the class under test doesn't need setup

### Mocking Strategy
**Critical Rules:**
- **NEVER mock domain objects** - Domain objects should be real instances
- **Fake over mock** - If a dependency is a functional interface or single-method interface, create a fake implementation instead of mocking
- **Mock external dependencies** - Services, repositories, and infrastructure components should be mocked
- Use static `mock()` methods, not `@Mock` or `@InjectMocks` annotations
- Inline simple construction as `private final` fields instead of using `@BeforeEach`
- **Be explicit in `when()` blocks** - Use specific parameter values instead of `any()` matchers when the parameter matters to the test

### Assertion Guidelines
- **Always use AssertJ** - Never use JUnit assertions
- Chain assertions fluently for readability
- Use soft assertions when validating multiple conditions on the same object
- Prefer specific assertions over generic ones (e.g., `hasSize()` over `satisfies()`)
- Use `assertThatCode()` for verifying no exception is thrown
- Leverage `usingRecursiveComparison()` for complex object equality
- Use `extracting()` with multiple fields for cleaner assertions
- Consider custom assertions for domain-specific validation

### Test Data Management
- Check for existing test builders or fixtures before creating domain objects
- If no builder exists for commonly used domain objects, create one using the Builder pattern
- Use meaningful test data that clarifies the test's intent
- Avoid test data pollution - use minimal data needed for the test

### Test Organization
- Use `@Nested` classes to group related test scenarios and improve readability
- Nested classes provide better structure for testing different states or contexts
- Each nested class can have its own setup specific to that scenario

## Implementation Guide

### 1. Basic Test Structure

```java
class ServiceUnderTestTest {
    
    private final ExternalService externalService = mock(ExternalService.class);
    private final ServiceUnderTest serviceUnderTest = new ServiceUnderTest(externalService);
    
    @Test
    void should_return_processed_result_when_input_is_valid() {
        // given
        var input = new Input("valid-data");
        when(externalService.process(input)).thenReturn("processed");
        
        // when
        var result = serviceUnderTest.execute(input);
        
        // then
        assertThat(result)
            .isNotNull()
            .extracting(Result::getValue)
            .isEqualTo("processed");
    }
}
```

### 2. Faking Functional Interfaces

Instead of mocking:
```java
// ❌ DON'T
@Mock
private Validator<String> validator;

@Test
void test_validation() {
    when(validator.validate("test")).thenReturn(ValidationResult.valid());
    // ...
}
```

Create a fake:
```java
// ✅ DO
@Test
void should_accept_valid_input() {
    // given
    Validator<String> validator = input -> 
        input.length() > 5 ? ValidationResult.valid() : ValidationResult.invalid("Too short");
    var service = new ServiceUnderTest(validator);
    
    // when
    var result = service.process("valid-input");
    
    // then
    assertThat(result).isNotNull();
}
```

### 3. Domain Objects - Never Mock

```java
// ❌ DON'T
@Mock
private User user;

@Test
void test_user_processing() {
    when(user.getName()).thenReturn("John");
    when(user.getAge()).thenReturn(30);
    // ...
}
```

```java
// ✅ DO - Use real domain objects
@Test
void should_process_adult_user() {
    // given
    var user = User.builder()
        .name("John")
        .age(30)
        .build();
    
    // when
    var result = service.processUser(user);
    
    // then
    assertThat(result.isAdult()).isTrue();
}
```

### 4. Test Builders

Create builders for complex domain objects:

```java
public class OrderTestBuilder {
    private String orderId = "default-id";
    private List<OrderItem> items = new ArrayList<>();
    private OrderStatus status = OrderStatus.PENDING;
    private LocalDateTime createdAt = LocalDateTime.now();
    
    public static OrderTestBuilder anOrder() {
        return new OrderTestBuilder();
    }
    
    public OrderTestBuilder withId(String orderId) {
        this.orderId = orderId;
        return this;
    }
    
    public OrderTestBuilder withItems(OrderItem... items) {
        this.items = Arrays.asList(items);
        return this;
    }
    
    public OrderTestBuilder withStatus(OrderStatus status) {
        this.status = status;
        return this;
    }
    
    public OrderTestBuilder completed() {
        this.status = OrderStatus.COMPLETED;
        return this;
    }
    
    public Order build() {
        return new Order(orderId, items, status, createdAt);
    }
}

// Usage in tests
@Test
void should_calculate_total_for_completed_order() {
    // given
    var order = anOrder()
        .withItems(
            new OrderItem("item-1", Money.of(10.00)),
            new OrderItem("item-2", Money.of(20.00))
        )
        .completed()
        .build();
    
    // when
    var total = service.calculateTotal(order);
    
    // then
    assertThat(total).isEqualTo(Money.of(30.00));
}
```

### 5. AssertJ Chaining

```java
@Test
void should_return_filtered_and_sorted_users() {
    // given
    var users = List.of(
        new User("Alice", 30),
        new User("Bob", 25),
        new User("Charlie", 35)
    );
    
    // when
    var result = service.getAdultUsersSortedByAge(users);
    
    // then
    assertThat(result)
        .hasSize(3)
        .extracting(User::getName)
        .containsExactly("Bob", "Alice", "Charlie");
}

@Test
void should_create_valid_response_with_all_fields() {
    // given
    var request = new Request("data");
    
    // when
    var response = service.handle(request);
    
    // then
    assertThat(response)
        .isNotNull()
        .satisfies(r -> {
            assertThat(r.getStatus()).isEqualTo(Status.SUCCESS);
            assertThat(r.getMessage()).isNotEmpty();
            assertThat(r.getTimestamp()).isBeforeOrEqualTo(LocalDateTime.now());
        });
}
```

### 6. Testing Exceptions

```java
@Test
void should_throw_exception_when_input_is_invalid() {
    // given
    var invalidInput = new Input(null);
    
    // when / then
    assertThatThrownBy(() -> service.process(invalidInput))
        .isInstanceOf(InvalidInputException.class)
        .hasMessage("Input cannot be null")
        .hasNoCause();
}

@Test
void should_throw_exception_with_proper_context() {
    // given
    var input = new Input("invalid");
    
    // when / then
    assertThatExceptionOfType(ValidationException.class)
        .isThrownBy(() -> service.validate(input))
        .withMessageContaining("invalid")
        .satisfies(ex -> {
            assertThat(ex.getErrorCode()).isEqualTo("VALIDATION_FAILED");
            assertThat(ex.getFields()).contains("input");
        });
}
```

### 7. Parameterized Tests

```java
@ParameterizedTest
@MethodSource("provideInvalidInputs")
void should_reject_invalid_inputs(String input, String expectedError) {
    // when / then
    assertThatThrownBy(() -> service.process(input))
        .isInstanceOf(ValidationException.class)
        .hasMessageContaining(expectedError);
}

private static Stream<Arguments> provideInvalidInputs() {
    return Strea

Related in Backend & APIs