From java-foundation
JUnit 5, Mockito, AssertJ, and Testcontainers patterns for any JVM project. Covers test structure, parameterised tests, mocking discipline, fluent assertions, and integration testing with real containers. Stack-agnostic — referenced by every Java plugin in the marketplace. Use this skill to: - Write clear, maintainable unit tests with JUnit 5. - Mock dependencies with Mockito without overusing mocks. - Write expressive assertions with AssertJ. - Spin up real infrastructure (DBs, message brokers) with Testcontainers for integration tests. Do NOT use this skill for: - Spring-specific test slices (@SpringBootTest, @WebMvcTest, @DataJpaTest — those are in spring-boot-plugin skills). - Framework-specific mocking utilities (MockMvc, WebTestClient).
How this skill is triggered — by the user, by Claude, or both
Slash command
/java-foundation:jvm-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
```java
import org.junit.jupiter.api.*;
import static org.assertj.core.api.Assertions.*;
class UserServiceTest {
private UserRepository repository;
private UserService service;
@BeforeEach
void setUp() {
repository = mock(UserRepository.class);
service = new UserService(repository);
}
@Test
void registerUser_withValidData_returnsActiveUser() {
// Arrange
var command = new RegisterUserCommand("[email protected]", "Secret1!");
when(repository.existsByEmail("[email protected]")).thenReturn(false);
when(repository.save(any())).thenAnswer(inv -> inv.getArgument(0));
// Act
var user = service.register(command);
// Assert
assertThat(user.email()).isEqualTo("[email protected]");
assertThat(user.isActive()).isTrue();
}
@Test
void registerUser_withDuplicateEmail_throwsException() {
when(repository.existsByEmail(any())).thenReturn(true);
assertThatThrownBy(() -> service.register(new RegisterUserCommand("[email protected]", "pass")))
.isInstanceOf(DuplicateEmailException.class)
.hasMessageContaining("[email protected]");
}
}
Test method naming: methodName_condition_expectedOutcome (readable without comments). @DisplayName for BDD-style prose when the method name would be unwieldy.
AAA structure: Arrange → Act → Assert. Separate with blank lines (no comments needed when the structure is clear).
One assertion concept per test. Multiple assertThat calls are fine when they all verify the same behaviour.
@ParameterizedTest
@ValueSource(strings = { "", " ", "\t", "\n" })
void isBlank_variousBlankStrings_returnsTrue(String input) {
assertThat(StringUtils.isBlank(input)).isTrue();
}
@ParameterizedTest
@CsvSource({
"[email protected], true",
"not-an-email, false",
"@nodomain.com, false",
})
void isValidEmail(String email, boolean expected) {
assertThat(validator.isValid(email)).isEqualTo(expected);
}
@ParameterizedTest
@MethodSource("invalidCommands")
void register_withInvalidCommand_throws(RegisterUserCommand command) {
assertThatThrownBy(() -> service.register(command))
.isInstanceOf(ValidationException.class);
}
static Stream<RegisterUserCommand> invalidCommands() {
return Stream.of(
new RegisterUserCommand(null, "pass"),
new RegisterUserCommand("", "pass"),
new RegisterUserCommand("[email protected]", "")
);
}
Prefer AssertJ over JUnit's assertEquals — it gives better failure messages and supports fluent chaining.
// Collections
assertThat(users)
.hasSize(3)
.extracting(User::email)
.containsExactlyInAnyOrder("[email protected]", "[email protected]", "[email protected]");
// Exceptions
assertThatThrownBy(() -> service.delete(unknownId))
.isInstanceOf(EntityNotFoundException.class)
.hasMessageContaining(unknownId.toString());
// Optional
assertThat(service.findById(42L))
.isPresent()
.hasValueSatisfying(u -> assertThat(u.name()).isEqualTo("Alice"));
// Soft assertions — collect all failures
assertSoftly(softly -> {
softly.assertThat(order.status()).isEqualTo(OrderStatus.CONFIRMED);
softly.assertThat(order.total()).isEqualByComparingTo("99.99");
softly.assertThat(order.items()).hasSize(2);
});
Never use Assertions.assertTrue(a.equals(b)) — the failure message shows only "expected true" with no values. Use assertThat(a).isEqualTo(b).
// Constructor injection — preferred (no reflection magic, works without Spring)
var repo = mock(UserRepository.class);
var service = new UserService(repo);
// Stubbing — be specific
when(repo.findById(42L)).thenReturn(Optional.of(testUser));
// Argument matchers — use when value doesn't matter; mix carefully
when(repo.existsByEmail(anyString())).thenReturn(false);
// Capture for verification
var captor = ArgumentCaptor.forClass(User.class);
verify(repo).save(captor.capture());
assertThat(captor.getValue().email()).isEqualTo("[email protected]");
// Verify interaction count
verify(repo, times(1)).save(any());
verify(repo, never()).delete(any());
Avoid @InjectMocks when possible — prefer explicit constructor injection in tests; it makes dependencies visible and avoids field-injection surprises.
Do not mock value objects or simple data classes. Only mock boundaries (repositories, HTTP clients, external services).
@Testcontainers
class UserRepositoryIT {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@BeforeAll
static void configure() {
// Wire datasource — exact mechanism depends on framework
System.setProperty("spring.datasource.url", postgres.getJdbcUrl());
System.setProperty("spring.datasource.username", postgres.getUsername());
System.setProperty("spring.datasource.password", postgres.getPassword());
}
@Test
void saveAndFindById_roundtrip() {
var repo = buildRepository();
var user = new User(null, "[email protected]");
var saved = repo.save(user);
var found = repo.findById(saved.id());
assertThat(found).isPresent()
.hasValueSatisfying(u -> assertThat(u.email()).isEqualTo("[email protected]"));
}
}
Static containers (static field + @Container) are reused across all tests in the class — faster than per-test containers. Reuse across test classes via a shared base class or singleton pattern.
Separate integration tests from unit tests. Maven Failsafe plugin runs *IT.java in verify; Gradle can use source sets or a custom test task.
<!-- Maven: integration tests run in verify phase -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
src/
├── main/java/com/example/...
└── test/java/com/example/
├── unit/ # optional subpackage grouping
│ └── UserServiceTest.java
└── integration/
└── UserRepositoryIT.java
Mirror the main package structure — each class under test has a corresponding test class in the same package hierarchy.
Test class naming:
{Subject}Test{Subject}IT{Subject}Tests (Spring convention)Aim for ≥ 80 % line coverage on business logic (services, domain objects). Framework glue code (configuration, main class) is excluded. Use JaCoCo to measure:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
npx claudepluginhub aratkruglik/claude-sdlc --plugin java-foundationGenerates production-grade JUnit 5 unit and integration tests in Java with assertions, parameterized tests, lifecycle hooks, Mockito mocking, and nested tests.
Generates JUnit 5 unit tests with Mockito and Testcontainers integration tests for Java services, repositories, controllers, and utilities. Auto-detects Maven/Gradle/Spring Boot setup from build files.
Provides Spring Boot testing patterns for unit (Mockito), slice (@DataJpaTest/@WebMvcTest), integration (@SpringBootTest), and Testcontainers-based tests with JUnit 5. Use when writing @Test methods, @MockBean mocks, or test suites.