Java’s testing ecosystem is mature and opinionated: JUnit 5 provides the test framework with parameterized tests and extensions, Mockito handles mocking, Spring Boot’s test slices (@WebMvcTest, @DataJpaTest, @SpringBootTest) load only the relevant application slices, and TestContainers spins up real databases in Docker. ArchUnit enforces architectural rules — no service layer depending on web layer — as tests. Claude Code generates complete test suites, MockMvc request/response tests, JPA repository slice tests, and TestContainers-based integration tests.
CLAUDE.md for Java Testing
## Testing Stack
- JUnit 5 (Jupiter) with @ExtendWith, @ParameterizedTest, @ValueSource
- Mockito 5.x — @Mock, @InjectMocks, when/thenReturn, verify
- Spring Boot Test 3.x — @SpringBootTest, @WebMvcTest, @DataJpaTest, @MockBean
- TestContainers 1.20+ — PostgreSQL, Redis, Kafka containers
- AssertJ for fluent assertions (not Hamcrest)
- ArchUnit for architecture tests (layer dependencies, naming conventions)
- JaCoCo for coverage — minimum 80% enforced in Maven/Gradle
- WireMock for HTTP dependency mocking
JUnit 5 Parameterized Tests
// src/test/java/com/myapp/service/OrderServiceTest.java
package com.myapp.service;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.BDDMockito.*;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private InventoryService inventoryService;
@InjectMocks
private OrderService orderService;
@ParameterizedTest
@ValueSource(ints = {1, 5, 10, 99, 100})
@DisplayName("Should accept valid quantities")
void shouldAcceptValidQuantities(int quantity) {
// given
given(inventoryService.checkStock("prod_abc", quantity)).willReturn(true);
given(orderRepository.save(any())).willAnswer(inv -> inv.getArgument(0));
// when/then
assertThatNoException().isThrownBy(() ->
orderService.createOrder("cust_123", "prod_abc", quantity)
);
}
@ParameterizedTest
@ValueSource(ints = {0, -1, -100, 101, Integer.MAX_VALUE})
@DisplayName("Should reject invalid quantities")
void shouldRejectInvalidQuantities(int quantity) {
assertThatThrownBy(() ->
orderService.createOrder("cust_123", "prod_abc", quantity)
).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("quantity");
}
// Complex parameterized test with custom stream
@ParameterizedTest
@MethodSource("discountTestCases")
@DisplayName("Should calculate correct discount")
void shouldCalculateCorrectDiscount(int totalCents, int expectedDiscountCents) {
int actual = orderService.calculateDiscount(totalCents);
assertThat(actual).isEqualTo(expectedDiscountCents);
}
static Stream<Arguments> discountTestCases() {
return Stream.of(
Arguments.of(4999, 0), // Under $50: no discount
Arguments.of(5000, 250), // $50: 5%
Arguments.of(10000, 1000), // $100: 10%
Arguments.of(50000, 7500), // $500: 15%
Arguments.of(100000, 20000) // $1000+: 20% (max)
);
}
@Test
@DisplayName("Should throw InsufficientStockException when product is out of stock")
void shouldThrowWhenOutOfStock() {
// given
given(inventoryService.checkStock("prod_abc", 5)).willReturn(false);
// when/then
assertThatThrownBy(() ->
orderService.createOrder("cust_123", "prod_abc", 5)
).isInstanceOf(InsufficientStockException.class)
.hasMessage("Product prod_abc has insufficient stock for quantity 5")
.satisfies(e -> assertThat(((InsufficientStockException) e).getProductId()).isEqualTo("prod_abc"));
// Verify no order was saved
verify(orderRepository, never()).save(any());
}
@Nested
@DisplayName("When cancelling orders")
class CancelOrderTests {
@Test
void shouldCancelPendingOrder() {
// given
var order = Order.builder().id("ord_123").status(OrderStatus.PENDING).build();
given(orderRepository.findById("ord_123")).willReturn(Optional.of(order));
given(orderRepository.save(any())).willAnswer(inv -> inv.getArgument(0));
// when
Order cancelled = orderService.cancelOrder("ord_123", "Customer request");
// then
assertThat(cancelled.getStatus()).isEqualTo(OrderStatus.CANCELLED);
assertThat(cancelled.getCancelReason()).isEqualTo("Customer request");
}
@ParameterizedTest
@EnumSource(value = OrderStatus.class, names = {"SHIPPED", "DELIVERED"})
void shouldNotCancelNonCancellableOrders(OrderStatus status) {
var order = Order.builder().id("ord_123").status(status).build();
given(orderRepository.findById("ord_123")).willReturn(Optional.of(order));
assertThatThrownBy(() -> orderService.cancelOrder("ord_123", "reason"))
.isInstanceOf(OrderNotCancellableException.class);
}
}
}
Spring MVC Test
// src/test/java/com/myapp/controller/OrderControllerTest.java
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Autowired
private ObjectMapper objectMapper;
@Test
void shouldCreateOrderAndReturn201() throws Exception {
// given
var input = new CreateOrderRequest("cust_123",
List.of(new OrderItem("prod_abc", 2)));
var createdOrder = Order.builder()
.id("ord_xyz")
.customerId("cust_123")
.status(OrderStatus.PENDING)
.build();
given(orderService.createOrder(any())).willReturn(createdOrder);
// when/then
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(input)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value("ord_xyz"))
.andExpect(jsonPath("$.status").value("PENDING"))
.andExpect(header().string("Location", containsString("/api/orders/ord_xyz")));
}
@Test
void shouldReturn400ForInvalidInput() throws Exception {
var invalidInput = new CreateOrderRequest(null, List.of()); // Null customerId, empty items
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(invalidInput)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors[?(@.field=='customerId')]").exists());
}
}
TestContainers Integration Test
// src/test/java/com/myapp/repository/OrderRepositoryIntegrationTest.java
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
@Testcontainers
class OrderRepositoryIntegrationTest {
@Container
@ServiceConnection // Spring Boot 3.1+ auto-configures datasource from container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Autowired
private OrderRepository orderRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldPersistAndRetrieveOrder() {
// given
var order = Order.builder()
.id(UUID.randomUUID().toString())
.customerId("cust_123")
.status(OrderStatus.PENDING)
.totalCents(4999)
.createdAt(Instant.now())
.build();
// when
entityManager.persist(order);
entityManager.flush();
entityManager.clear(); // Ensure clean fetch from DB
var found = orderRepository.findById(order.getId());
// then
assertThat(found).isPresent();
assertThat(found.get().getCustomerId()).isEqualTo("cust_123");
assertThat(found.get().getTotalCents()).isEqualTo(4999);
}
@Test
void shouldFindOrdersByCustomerIdSortedByDate() {
// ... create multiple orders, verify ordering
}
}
ArchUnit Architecture Tests
// src/test/java/com/myapp/ArchitectureTest.java
@AnalyzeClasses(packages = "com.myapp")
class ArchitectureTest {
@ArchTest
ArchRule servicesShouldNotDependOnControllers = noClasses()
.that().resideInAPackage("..service..")
.should().dependOnClassesThat()
.resideInAPackage("..controller..");
@ArchTest
ArchRule repositoriesShouldOnlyBeAccessedFromServices = classes()
.that().resideInAPackage("..repository..")
.should().onlyBeAccessed()
.byClassesThat().resideInAnyPackage("..service..", "..repository..");
@ArchTest
ArchRule servicesShouldBeAnnotated = classes()
.that().resideInAPackage("..service..")
.and().haveSimpleNameEndingWith("Service")
.should().beAnnotatedWith(Service.class);
@ArchTest
ArchRule noCircularDependencies = SlicesRuleDefinition.slices()
.matching("com.myapp.(*)..")
.should().beFreeOfCycles();
}
For the Spring Boot application that these tests cover, see the Java Spring Boot guide for controller, service, and repository layer patterns. For the Kotlin coroutines alternative to Java for Android and backend development, the Android Kotlin guide covers Kotlin-first patterns. The Claude Skills 360 bundle includes Java testing skill sets covering JUnit 5 parameterized tests, Spring Boot test slices, and TestContainers integration. Start with the free tier to try Java test generation.