Skip to main content

testing-patterns

Testing patterns for Spring Boot - JUnit 5, Mockito, MockMvc, Testcontainers, positive and negative test cases. Use when user asks about testing, writing tests, test coverage, or test failures.

Source facts

Repository
piomin/claude-ai-spring-boot
Last source activity
October 18, 2026 at 09:39
Detected SKILL.md language
English
Stars
1,430
Forks
448

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
testing-patterns
description
Testing patterns for Spring Boot - JUnit 5, Mockito, MockMvc, Testcontainers, positive and negative test cases. Use when user asks about testing, writing tests, test coverage, or test failures.
# Testing Patterns Skill Testing strategies for Spring Boot applications. Always write both positive and negative test cases. ## When to Use - "write tests" / "add test coverage" / "test this" - Test failures or debugging test issues - Choosing between unit, slice, and integration tests --- ## Test Pyramid | Level | Annotation | Speed | Scope | When to Use | |-------|-----------|-------|-------|-------------| | Unit | `@ExtendWith(MockitoExtension.class)` | Fast | Single class | Business logic in services | | Slice | `@WebMvcTest`, `@DataJpaTest` | Medium | One layer | Controllers, repositories | | Integration | `@SpringBootTest` | Slow | Full app | End-to-end flows, cross-layer | --- ## Unit Tests (Service Layer) ```java @ExtendWith(MockitoExtension.class) class OrderServiceTest { @Mock private OrderRepository orderRepository; @Mock private ProductService productService; @InjectMocks private OrderService orderService; // ✅ Positive test @Test @DisplayName("Should create order with valid items") void shouldCreateOrder() { // Given var request = new OrderRequest(List.of( new OrderItem(1L, 2) )); var product = new Product(); product.setId(1L); product.setPrice(new BigDecimal("10.00")); when(productService.findById(1L)).thenReturn(product); when(orderRepository.save(any(Order.class))).thenAnswer(inv -> { Order o = inv.getArgument(0); o.setId(1L); return o; }); // When Order result = orderService.create(request); // Then assertThat(result.getId()).isEqualTo(1L); verify(orderRepository).save(any(Order.class)); } // ✅ Negative test @Test @DisplayName("Should throw when product not found") void shouldThrowWhenProductNotFound() { // Given var request = new OrderRequest(List.of( new OrderItem(999L, 1) )); when(productService.findById(999L)) .thenThrow(new EntityNotFoundException("Product not found")); // When & Then assertThatThrownBy(() -> orderService.create(request)) .isInstanceOf(EntityNotFoundException.class) .hasMessageContaining("Product not found"); verify(orderRepository, never()).save(any()); } // ✅ Negative test - empty input @Test @DisplayName("Should throw when order has no items") void shouldThrowWhenNoItems() { var request = new OrderRequest(List.of()); assertThatThrownBy(() -> orderService.create(request)) .isInstanceOf(IllegalArgumentException.class); } } ``` --- ## Controller Slice Tests (@WebMvcTest) ```java @WebMvcTest(ProductController.class) class ProductControllerTest { @Autowired private MockMvc mockMvc; @MockBean private ProductService productService; @Autowired private ObjectMapper objectMapper; // ✅ Positive - successful creation @Test @DisplayName("POST /api/v1/products - returns 201") void createProduct_validRequest_returns201() throws Exception { var request = new ProductRequest("Widget", new BigDecimal("10.00")); var product = new Product(); product.setId(1L); product.setName("Widget"); when(productService.create(any())).thenReturn(product); mockMvc.perform(post("/api/v1/products") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.name").value("Widget")); } // ✅ Negative - validation failure @Test @DisplayName("POST /api/v1/products - blank name returns 400") void createProduct_blankName_returns400() throws Exception { var request = new ProductRequest("", new BigDecimal("10.00")); mockMvc.perform(post("/api/v1/products") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.name").exists()); verify(productService, never()).create(any()); } // ✅ Negative - not found @Test @DisplayName("GET /api/v1/products/999 - returns 404") void getProduct_notFound_returns404() throws Exception { when(productService.findById(999L)) .thenThrow(new EntityNotFoundException("Product not found")); mockMvc.perform(get("/api/v1/products/999")) .andExpect(status().isNotFound()); } } ``` --- ## Repository Slice Tests (@DataJpaTest) ```java @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @Testcontainers class ProductRepositoryTest { @Container @ServiceConnection static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine"); @Autowired private ProductRepository productRepository; @Autowired private TestEntityManager entityManager; @Test @DisplayName("Should find products by name containing") void shouldFindByNameContaining() { // Given var product = new Product(); product.setName("Spring Widget"); product.setPrice(new BigDecimal("10.00")); entityManager.persistAndFlush(product); // When List<Product> results = productRepository .findByNameContainingIgnoreCase("widget"); // Then assertThat(results).hasSize(1); assertThat(results.get(0).getName()).isEqualTo("Spring Widget"); } @Test @DisplayName("Should return empty list when no match") void shouldReturnEmptyWhenNoMatch() { List<Product> results = productRepository .findByNameContainingIgnoreCase("nonexistent"); assertThat(results).isEmpty(); } } ``` --- ## Integration Tests (@SpringBootTest) ```java @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureRestTestClient @Testcontainers @ActiveProfiles("test") class ProductIntegrationTest { @Container @ServiceConnection static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine"); @Autowired private RestTestClient restTestClient; @Autowired private ProductRepository productRepository; @BeforeEach void setUp() { productRepository.deleteAll(); } @Test @DisplayName("Full CRUD flow works end-to-end") void crudFlow() { // Create var request = new ProductRequest("Widget", new BigDecimal("10.00")); var created = restTestClient.post() .uri("/api/v1/products") .contentType(MediaType.APPLICATION_JSON) .body(request) .exchange() .expectStatus().isCreated() .expectBody(ProductResponse.class) .returnResult() .getResponseBody(); Long id = created.id(); // Read restTestClient.get() .uri("/api/v1/products/{id}", id) .exchange() .expectStatus().isOk() .expectBody(ProductResponse.class) .value(product -> assertThat(product.name()).isEqualTo("Widget")); // Delete restTestClient.delete() .uri("/api/v1/products/{id}", id) .exchange() .expectStatus().isNoContent(); restTestClient.get() .uri("/api/v1/products/{id}", id) .exchange() .expectStatus().isNotFound(); } } ``` --- ## Testcontainers Reusable Config ```java @TestConfiguration(proxyBeanMethods = false) public class TestcontainersConfig { @Bean @ServiceConnection public PostgreSQLContainer<?> postgresContainer() { return new PostgreSQLContainer<>("postgres:16-alpine"); } } // Use with @Import @SpringBootTest @Import(TestcontainersConfig.class) class AnyIntegrationTest { // ... } ``` --- ## Parameterized Tests ```java @ParameterizedTest @ValueSource(strings = {"", " ", "ab"}) @DisplayName("Should reject invalid usernames") void shouldRejectInvalidUsername(String username) { var request = new UserRequest(username, "valid@email.com"); assertThatThrownBy(() -> userService.create(request)) .isInstanceOf(ConstraintViolationException.class); } @ParameterizedTest @CsvSource({ "admin@test.com, ADMIN, true", "user@test.com, USER, true", "guest@test.com, GUEST, false" }) @DisplayName("Should check access by role") void shouldCheckAccessByRole(String email, String role, boolean expected) { assertThat(securityService.hasAccess(email, role)).isEqualTo(expected); } ``` --- ## Quick Reference | Pattern | Use For | |---------|---------| | `@ExtendWith(MockitoExtension.class)` | Unit tests with mocks | | `@WebMvcTest(Controller.class)` | Controller tests with MockMvc | | `@DataJpaTest` | Repository tests with real DB | | `@SpringBootTest` + `RANDOM_PORT` | Full integration tests | | `@ServiceConnection` | Auto-configure Testcontainers (3.1+) | | `@ParameterizedTest` | Multiple inputs, same assertion | | `assertThatThrownBy()` | Negative test - expect exception | | `verify(mock, never())` | Verify method was NOT called | ## Naming Convention ``` methodUnderTest_condition_expectedResult ``` Examples: `createUser_validRequest_returns201`, `createUser_duplicateEmail_throws409`
View on GitHub