diff --git a/docs/README.skills.md b/docs/README.skills.md
index 26182de0..4c7ecca5 100644
--- a/docs/README.skills.md
+++ b/docs/README.skills.md
@@ -235,6 +235,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
| [shuffle-json-data](../skills/shuffle-json-data/SKILL.md) | Shuffle repetitive JSON objects safely by validating schema consistency before randomising entries. | None |
| [snowflake-semanticview](../skills/snowflake-semanticview/SKILL.md) | Create, alter, and validate Snowflake semantic views using Snowflake CLI (snow). Use when asked to build or troubleshoot semantic views/semantic layer definitions with CREATE/ALTER SEMANTIC VIEW, to validate semantic-view DDL against Snowflake via CLI, or to guide Snowflake CLI installation and connection setup. | None |
| [sponsor-finder](../skills/sponsor-finder/SKILL.md) | Find which of a GitHub repository's dependencies are sponsorable via GitHub Sponsors. Uses deps.dev API for dependency resolution across npm, PyPI, Cargo, Go, RubyGems, Maven, and NuGet. Checks npm funding metadata, FUNDING.yml files, and web search. Verifies every link. Shows direct and transitive dependencies with OSSF Scorecard health data. Invoke with /sponsor followed by a GitHub owner/repo (e.g. "/sponsor expressjs/express"). | None |
+| [spring-boot-testing](../skills/spring-boot-testing/SKILL.md) | Expert Spring Boot 4 testing specialist that selects the best Spring Boot testing techniques for your situation with Junit 6 and AssertJ. | `references/assertj-basics.md`
`references/assertj-collections.md`
`references/context-caching.md`
`references/datajpatest.md`
`references/instancio.md`
`references/mockitobean.md`
`references/mockmvc-classic.md`
`references/mockmvc-tester.md`
`references/restclienttest.md`
`references/resttestclient.md`
`references/sb4-migration.md`
`references/test-slices-overview.md`
`references/testcontainers-jdbc.md`
`references/webmvctest.md` |
| [sql-code-review](../skills/sql-code-review/SKILL.md) | Universal SQL code review assistant that performs comprehensive security, maintainability, and code quality analysis across all SQL databases (MySQL, PostgreSQL, SQL Server, Oracle). Focuses on SQL injection prevention, access control, code standards, and anti-pattern detection. Complements SQL optimization prompt for complete development coverage. | None |
| [sql-optimization](../skills/sql-optimization/SKILL.md) | Universal SQL performance optimization assistant for comprehensive query tuning, indexing strategies, and database performance analysis across all SQL databases (MySQL, PostgreSQL, SQL Server, Oracle). Provides execution plan analysis, pagination optimization, batch operations, and performance monitoring guidance. | None |
| [structured-autonomy-generate](../skills/structured-autonomy-generate/SKILL.md) | Structured Autonomy Implementation Generator Prompt | None |
diff --git a/skills/spring-boot-testing/SKILL.md b/skills/spring-boot-testing/SKILL.md
new file mode 100644
index 00000000..9e97e983
--- /dev/null
+++ b/skills/spring-boot-testing/SKILL.md
@@ -0,0 +1,189 @@
+---
+name: spring-boot-testing
+description: Expert Spring Boot 4 testing specialist that selects the best Spring Boot testing techniques for your situation with Junit 6 and AssertJ.
+---
+
+# Spring Boot Testing
+
+This skill provides expert guide for testing Spring Boot 4 applications with modern patterns and best practices.
+
+## Core Principles
+
+1. **Test Pyramid**: Unit (fast) > Slice (focused) > Integration (complete)
+2. **Right Tool**: Use the narrowest slice that gives you confidence
+3. **AssertJ Style**: Fluent, readable assertions over verbose matchers
+4. **Modern APIs**: Prefer MockMvcTester and RestTestClient over legacy alternatives
+
+## Which Test Slice?
+
+| Scenario | Annotation | Reference |
+|----------|------------|-----------|
+| Controller + HTTP semantics | `@WebMvcTest` | [references/webmvctest.md](references/webmvctest.md) |
+| Repository + JPA queries | `@DataJpaTest` | [references/datajpatest.md](references/datajpatest.md) |
+| REST client + external APIs | `@RestClientTest` | [references/restclienttest.md](references/restclienttest.md) |
+| JSON (de)serialization | `@JsonTest` | [references/test-slices-overview.md](references/test-slices-overview.md) |
+| Full application | `@SpringBootTest` | [references/test-slices-overview.md](references/test-slices-overview.md) |
+
+## Test Slices Reference
+
+- [references/test-slices-overview.md](references/test-slices-overview.md) - Decision matrix and comparison
+- [references/webmvctest.md](references/webmvctest.md) - Web layer with MockMvc
+- [references/datajpatest.md](references/datajpatest.md) - Data layer with Testcontainers
+- [references/restclienttest.md](references/restclienttest.md) - REST client testing
+
+## Testing Tools Reference
+
+- [references/mockmvc-tester.md](references/mockmvc-tester.md) - AssertJ-style MockMvc (3.2+)
+- [references/mockmvc-classic.md](references/mockmvc-classic.md) - Traditional MockMvc (pre-3.2)
+- [references/resttestclient.md](references/resttestclient.md) - Spring Boot 4+ REST client
+- [references/mockitobean.md](references/mockitobean.md) - Mocking dependencies
+
+## Assertion Libraries
+
+- [references/assertj-basics.md](references/assertj-basics.md) - Scalars, strings, booleans, dates
+- [references/assertj-collections.md](references/assertj-collections.md) - Lists, Sets, Maps, arrays
+
+## Testcontainers
+
+- [references/testcontainers-jdbc.md](references/testcontainers-jdbc.md) - PostgreSQL, MySQL, etc.
+
+## Test Data Generation
+
+- [references/instancio.md](references/instancio.md) - Generate complex test objects (3+ properties)
+
+## Performance & Migration
+
+- [references/context-caching.md](references/context-caching.md) - Speed up test suites
+- [references/sb4-migration.md](references/sb4-migration.md) - Spring Boot 4.0 changes
+
+## Quick Decision Tree
+
+```
+Testing a controller endpoint?
+ Yes → @WebMvcTest with MockMvcTester
+
+Testing repository queries?
+ Yes → @DataJpaTest with Testcontainers (real DB)
+
+Testing business logic in service?
+ Yes → Plain JUnit + Mockito (no Spring context)
+
+Testing external API client?
+ Yes → @RestClientTest with MockRestServiceServer
+
+Testing JSON mapping?
+ Yes → @JsonTest
+
+Need full integration test?
+ Yes → @SpringBootTest with minimal context config
+```
+
+## Spring Boot 4 Highlights
+
+- **RestTestClient**: Modern alternative to TestRestTemplate
+- **@MockitoBean**: Replaces @MockBean (deprecated)
+- **MockMvcTester**: AssertJ-style assertions for web tests
+- **Modular starters**: Technology-specific test starters
+- **Context pausing**: Automatic pausing of cached contexts (Spring Framework 7)
+
+## Testing Best Practices
+
+### Code Complexity Assessment
+
+When a method or class is too complex to test effectively:
+
+1. **Analyze complexity** - If you need more than 5-7 test cases to cover a single method, it's likely too complex
+2. **Recommend refactoring** - Suggest breaking the code into smaller, focused functions
+3. **User decision** - If the user agrees to refactor, help identify extraction points
+4. **Proceed if needed** - If the user decides to continue with the complex code, implement tests despite the difficulty
+
+**Example of refactoring recommendation:**
+```java
+// Before: Complex method hard to test
+public Order processOrder(OrderRequest request) {
+ // Validation, discount calculation, payment, inventory, notification...
+ // 50+ lines of mixed concerns
+}
+
+// After: Refactored into testable units
+public Order processOrder(OrderRequest request) {
+ validateOrder(request);
+ var order = createOrder(request);
+ applyDiscount(order);
+ processPayment(order);
+ updateInventory(order);
+ sendNotification(order);
+ return order;
+}
+```
+
+### Avoid Code Redundancy
+
+Create helper methods for commonly used objects and mock setup to enhance readability and maintainability.
+
+### Test Organization with @DisplayName
+
+Use descriptive display names to clarify test intent:
+
+```java
+@Test
+@DisplayName("Should calculate discount for VIP customer")
+void shouldCalculateDiscountForVip() { }
+
+@Test
+@DisplayName("Should reject order when customer has insufficient credit")
+void shouldRejectOrderForInsufficientCredit() { }
+```
+
+### Test Coverage Order
+
+Always structure tests in this order:
+
+1. **Main scenario** - The happy path, most common use case
+2. **Other paths** - Alternative valid scenarios, edge cases
+3. **Exceptions/Errors** - Invalid inputs, error conditions, failure modes
+
+### Test Production Scenarios
+
+Write tests with real production scenarios in mind. This makes tests more relatable and helps understand code behavior in actual production cases.
+
+### Test Coverage Goals
+
+Aim for 80% code coverage as a practical balance between quality and effort. Higher coverage is beneficial but not the only goal.
+
+Use Jacoco maven plugin for coverage reporting and tracking.
+
+
+**Coverage Rules:**
+- 80+% coverage minimum
+- Focus on meaningful assertions, not just execution
+
+**What to Prioritize:**
+1. Business-critical paths (payment processing, order validation)
+2. Complex algorithms (pricing, discount calculations)
+3. Error handling (exceptions, edge cases)
+4. Integration points (external APIs, databases)
+
+## Dependencies (Spring Boot 4)
+
+```xml
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webmvc-test
+ test
+
+
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+```
diff --git a/skills/spring-boot-testing/references/assertj-basics.md b/skills/spring-boot-testing/references/assertj-basics.md
new file mode 100644
index 00000000..7d7f965e
--- /dev/null
+++ b/skills/spring-boot-testing/references/assertj-basics.md
@@ -0,0 +1,207 @@
+# AssertJ Basics
+
+Fluent assertions for readable, maintainable tests.
+
+## Basic Assertions
+
+### Object Equality
+
+```java
+assertThat(order.getStatus()).isEqualTo("PENDING");
+assertThat(order.getId()).isNotEqualTo(0);
+assertThat(order).isEqualTo(expectedOrder);
+assertThat(order).isNotNull();
+assertThat(nullOrder).isNull();
+```
+
+### String Assertions
+
+```java
+assertThat(order.getDescription())
+ .isEqualTo("Test Order")
+ .startsWith("Test")
+ .endsWith("Order")
+ .contains("Test")
+ .hasSize(10)
+ .matches("[A-Za-z ]+");
+```
+
+### Number Assertions
+
+```java
+assertThat(order.getAmount())
+ .isEqualTo(99.99)
+ .isGreaterThan(50)
+ .isLessThan(100)
+ .isBetween(50, 100)
+ .isPositive()
+ .isNotZero();
+```
+
+### Boolean Assertions
+
+```java
+assertThat(order.isActive()).isTrue();
+assertThat(order.isDeleted()).isFalse();
+```
+
+## Date/Time Assertions
+
+```java
+assertThat(order.getCreatedAt())
+ .isEqualTo(LocalDateTime.of(2024, 1, 15, 10, 30))
+ .isBefore(LocalDateTime.now())
+ .isAfter(LocalDateTime.of(2024, 1, 1))
+ .isCloseTo(LocalDateTime.now(), within(5, ChronoUnit.SECONDS));
+```
+
+## Optional Assertions
+
+```java
+Optional maybeOrder = orderService.findById(1L);
+
+assertThat(maybeOrder)
+ .isPresent()
+ .hasValueSatisfying(order -> {
+ assertThat(order.getId()).isEqualTo(1L);
+ });
+
+assertThat(orderService.findById(999L)).isEmpty();
+```
+
+## Exception Assertions
+
+### JUnit 5 Exception Handling
+
+```java
+@Test
+void shouldThrowException() {
+ OrderService service = new OrderService();
+
+ assertThatThrownBy(() -> service.findById(999L))
+ .isInstanceOf(OrderNotFoundException.class)
+ .hasMessage("Order 999 not found")
+ .hasMessageContaining("999");
+}
+```
+
+### AssertJ Exception Handling
+
+```java
+@Test
+void shouldThrowExceptionWithCause() {
+ assertThatExceptionOfType(OrderProcessingException.class)
+ .isThrownBy(() -> service.processOrder(invalidOrder))
+ .withCauseInstanceOf(ValidationException.class);
+}
+```
+
+## Custom Assertions
+
+Create domain-specific assertions for reusable test code:
+
+```java
+public class OrderAssert extends AbstractAssert {
+
+ public static OrderAssert assertThat(Order actual) {
+ return new OrderAssert(actual);
+ }
+
+ private OrderAssert(Order actual) {
+ super(actual, OrderAssert.class);
+ }
+
+ public OrderAssert isPending() {
+ isNotNull();
+ if (!"PENDING".equals(actual.getStatus())) {
+ failWithMessage("Expected order status to be PENDING but was %s", actual.getStatus());
+ }
+ return this;
+ }
+
+ public OrderAssert hasTotal(BigDecimal expected) {
+ isNotNull();
+ if (!expected.equals(actual.getTotal())) {
+ failWithMessage("Expected total %s but was %s", expected, actual.getTotal());
+ }
+ return this;
+ }
+}
+```
+
+Usage:
+
+```java
+OrderAssert.assertThat(order)
+ .isPending()
+ .hasTotal(new BigDecimal("99.99"));
+```
+
+## Soft Assertions
+
+Collect multiple failures before failing:
+
+```java
+@Test
+void shouldValidateOrder() {
+ Order order = orderService.findById(1L);
+
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(order.getId()).isEqualTo(1L);
+ softly.assertThat(order.getStatus()).isEqualTo("PENDING");
+ softly.assertThat(order.getItems()).isNotEmpty();
+ });
+}
+```
+
+## Satisfies Pattern
+
+```java
+assertThat(order)
+ .satisfies(o -> {
+ assertThat(o.getId()).isPositive();
+ assertThat(o.getStatus()).isNotBlank();
+ assertThat(o.getCreatedAt()).isNotNull();
+ });
+```
+
+## Using with Spring
+
+```java
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest
+class OrderServiceTest {
+
+ @Autowired
+ private OrderService orderService;
+
+ @Test
+ void shouldCreateOrder() {
+ Order order = orderService.create(new OrderRequest("Product", 2));
+
+ assertThat(order)
+ .isNotNull()
+ .extracting(Order::getId, Order::getStatus)
+ .containsExactly(1L, "PENDING");
+ }
+}
+```
+
+## Static Import
+
+Always use static import for clean assertions:
+
+```java
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.catchThrowable;
+```
+
+## Key Benefits
+
+1. **Readable**: Sentence-like structure
+2. **Type-safe**: IDE autocomplete works
+3. **Rich API**: Many built-in assertions
+4. **Extensible**: Custom assertions for your domain
+5. **Better Errors**: Clear failure messages
diff --git a/skills/spring-boot-testing/references/assertj-collections.md b/skills/spring-boot-testing/references/assertj-collections.md
new file mode 100644
index 00000000..6d38e75d
--- /dev/null
+++ b/skills/spring-boot-testing/references/assertj-collections.md
@@ -0,0 +1,183 @@
+# AssertJ Collections
+
+AssertJ assertions for collections: `List`, `Set`, `Map`, arrays, and streams.
+
+## When to Use This Reference
+
+- The value under test is a `List`, `Set`, `Map`, array, or `Stream`
+- You need to assert on multiple elements, their order, or specific fields within them
+- You are using `extracting()`, `filteredOn()`, `containsExactly()`, or similar collection methods
+- Asserting a single scalar or single object → use [assertj-basics.md](assertj-basics.md) instead
+
+## Basic Collection Checks
+
+```java
+List orders = orderService.findAll();
+
+assertThat(orders).isNotEmpty();
+assertThat(orders).isEmpty();
+assertThat(orders).hasSize(3);
+assertThat(orders).hasSizeGreaterThan(0);
+assertThat(orders).hasSizeLessThanOrEqualTo(10);
+```
+
+## Containment Assertions
+
+```java
+// Contains (any order, allows extras)
+assertThat(orders).contains(order1, order2);
+
+// Contains exactly these elements in this order (no extras)
+assertThat(statuses).containsExactly("NEW", "PENDING", "COMPLETED");
+
+// Contains exactly these elements in any order (no extras)
+assertThat(statuses).containsExactlyInAnyOrder("COMPLETED", "NEW", "PENDING");
+
+// Contains any of these elements (at least one match required)
+assertThat(statuses).containsAnyOf("NEW", "CANCELLED");
+
+// Does not contain
+assertThat(statuses).doesNotContain("DELETED");
+```
+
+## Extracting Fields
+
+Extract a single field from each element before asserting:
+
+```java
+assertThat(orders)
+ .extracting(Order::getStatus)
+ .containsExactly("NEW", "PENDING", "COMPLETED");
+```
+
+Extract multiple fields as tuples:
+
+```java
+assertThat(orders)
+ .extracting(Order::getId, Order::getStatus)
+ .containsExactly(
+ tuple(1L, "NEW"),
+ tuple(2L, "PENDING"),
+ tuple(3L, "COMPLETED")
+ );
+```
+
+## Filtering Before Asserting
+
+```java
+assertThat(orders)
+ .filteredOn(order -> order.getStatus().equals("PENDING"))
+ .hasSize(2)
+ .extracting(Order::getId)
+ .containsExactlyInAnyOrder(1L, 3L);
+
+// Filter by field value
+assertThat(orders)
+ .filteredOn("status", "PENDING")
+ .hasSize(2);
+```
+
+## Predicate Checks
+
+```java
+assertThat(orders).allMatch(o -> o.getTotal().compareTo(BigDecimal.ZERO) > 0);
+assertThat(orders).anyMatch(o -> o.getStatus().equals("COMPLETED"));
+assertThat(orders).noneMatch(o -> o.getStatus().equals("DELETED"));
+
+// With description for failure messages
+assertThat(orders)
+ .allSatisfy(o -> assertThat(o.getId()).isPositive());
+```
+
+## Per-Element Ordered Assertions
+
+Assert each element in order with individual conditions:
+
+```java
+assertThat(orders).satisfiesExactly(
+ first -> assertThat(first.getStatus()).isEqualTo("NEW"),
+ second -> assertThat(second.getStatus()).isEqualTo("PENDING"),
+ third -> {
+ assertThat(third.getStatus()).isEqualTo("COMPLETED");
+ assertThat(third.getTotal()).isGreaterThan(BigDecimal.ZERO);
+ }
+);
+```
+
+## Nested / Flat Collections
+
+```java
+// flatExtracting: flatten one level of nested collections
+assertThat(orders)
+ .flatExtracting(Order::getItems)
+ .extracting(OrderItem::getProduct)
+ .contains("Laptop", "Mouse");
+```
+
+## Recursive Field Comparison
+
+Compare elements by fields instead of object identity:
+
+```java
+assertThat(orders)
+ .usingRecursiveFieldByFieldElementComparator()
+ .containsExactlyInAnyOrder(expectedOrder1, expectedOrder2);
+
+// Ignore specific fields (e.g. generated IDs or timestamps)
+assertThat(orders)
+ .usingRecursiveFieldByFieldElementComparatorIgnoringFields("id", "createdAt")
+ .containsExactly(expectedOrder1, expectedOrder2);
+```
+
+## Map Assertions
+
+```java
+Map stockByProduct = inventoryService.getStock();
+
+assertThat(stockByProduct)
+ .isNotEmpty()
+ .hasSize(3)
+ .containsKey("Laptop")
+ .doesNotContainKey("Fax Machine")
+ .containsEntry("Laptop", 10)
+ .containsEntries(entry("Laptop", 10), entry("Mouse", 50));
+
+assertThat(stockByProduct)
+ .hasEntrySatisfying("Laptop", qty -> assertThat(qty).isGreaterThan(0));
+```
+
+## Array Assertions
+
+```java
+String[] roles = user.getRoles();
+
+assertThat(roles).hasSize(2);
+assertThat(roles).contains("ADMIN");
+assertThat(roles).containsExactlyInAnyOrder("USER", "ADMIN");
+```
+
+## Set Assertions
+
+```java
+Set tags = product.getTags();
+
+assertThat(tags).contains("electronics", "sale");
+assertThat(tags).doesNotContain("expired");
+assertThat(tags).hasSizeGreaterThanOrEqualTo(1);
+```
+
+## Static Import
+
+```java
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.assertj.core.api.Assertions.entry;
+```
+
+## Key Points
+
+1. **`containsExactly` vs `containsExactlyInAnyOrder`** — use the former when order matters
+2. **`extracting()` before containment checks** — avoids implementing `equals()` on domain objects
+3. **`filteredOn()` + `extracting()`** — compose to assert a subset of a collection precisely
+4. **`satisfiesExactly()`** — use when each element needs different assertions
+5. **`usingRecursiveFieldByFieldElementComparator()`** — preferred over `equals()` for DTOs and records
diff --git a/skills/spring-boot-testing/references/context-caching.md b/skills/spring-boot-testing/references/context-caching.md
new file mode 100644
index 00000000..0ee54b98
--- /dev/null
+++ b/skills/spring-boot-testing/references/context-caching.md
@@ -0,0 +1,115 @@
+# Context Caching
+
+Optimize Spring Boot test suite performance through context caching.
+
+## How Context Caching Works
+
+Spring's TestContext Framework caches application contexts based on their configuration "key". Tests with identical configurations reuse the same context.
+
+### What Affects the Cache Key
+
+- @ContextConfiguration
+- @TestPropertySource
+- @ActiveProfiles
+- @WebAppConfiguration
+- @MockitoBean definitions
+- @TestConfiguration imports
+
+## Cache Key Examples
+
+### Same Key (Context Reused)
+
+```java
+@WebMvcTest(OrderController.class)
+class OrderControllerTest1 {
+ @MockitoBean private OrderService orderService;
+}
+
+@WebMvcTest(OrderController.class)
+class OrderControllerTest2 {
+ @MockitoBean private OrderService orderService;
+}
+// Same context reused
+```
+
+### Different Key (New Context)
+
+```java
+@WebMvcTest(OrderController.class)
+@ActiveProfiles("test")
+class OrderControllerTest1 { }
+
+@WebMvcTest(OrderController.class)
+@ActiveProfiles("integration")
+class OrderControllerTest2 { }
+// Different contexts loaded
+```
+
+## Viewing Cache Statistics
+
+### Spring Boot Actuator
+
+```yaml
+management:
+ endpoints:
+ web:
+ exposure:
+ include: metrics
+```
+
+Access: `GET /actuator/metrics/spring.test.context.cache`
+
+### Debug Logging
+
+```properties
+logging.level.org.springframework.test.context.cache=DEBUG
+```
+
+## Optimizing Cache Hit Rate
+
+### Group Tests by Configuration
+
+```
+ tests/
+ unit/ # No context
+ web/ # @WebMvcTest
+ repository/ # @DataJpaTest
+ integration/ # @SpringBootTest
+```
+
+### Minimize @TestPropertySource Variations
+
+**Bad (multiple contexts):**
+
+```java
+@TestPropertySource(properties = "app.feature-x=true")
+class FeatureXTest { }
+
+@TestPropertySource(properties = "app.feature-y=true")
+class FeatureYTest { }
+```
+
+**Better (grouped):**
+
+```java
+@TestPropertySource(properties = {"app.feature-x=true", "app.feature-y=true"})
+class FeaturesTest { }
+```
+
+### Use @DirtiesContext Sparingly
+
+Only when context state truly changes:
+
+```java
+@Test
+@DirtiesContext // Forces context rebuild after test
+void testThatModifiesBeanDefinitions() { }
+```
+
+## Best Practices
+
+1. **Group by configuration** - Keep tests with same config together
+2. **Limit property variations** - Use profiles over individual properties
+3. **Avoid @DirtiesContext** - Prefer test data cleanup
+4. **Use narrow slices** - @WebMvcTest vs @SpringBootTest
+5. **Monitor cache hits** - Enable debug logging occasionally
diff --git a/skills/spring-boot-testing/references/datajpatest.md b/skills/spring-boot-testing/references/datajpatest.md
new file mode 100644
index 00000000..60de474d
--- /dev/null
+++ b/skills/spring-boot-testing/references/datajpatest.md
@@ -0,0 +1,197 @@
+# @DataJpaTest
+
+Testing JPA repositories with isolated data layer slice.
+
+## Basic Structure
+
+```java
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+@Testcontainers
+class OrderRepositoryTest {
+
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:18");
+
+ @Autowired
+ private OrderRepository orderRepository;
+
+ @Autowired
+ private TestEntityManager entityManager;
+}
+```
+
+## What Gets Loaded
+
+- Repository beans
+- EntityManager / TestEntityManager
+- DataSource
+- Transaction manager
+- No web layer, no services, no controllers
+
+## Testing Custom Queries
+
+```java
+@Test
+void shouldFindOrdersByStatus() {
+ // Given - Using var for cleaner code
+ var pending = new Order("PENDING");
+ var completed = new Order("COMPLETED");
+ entityManager.persist(pending);
+ entityManager.persist(completed);
+ entityManager.flush();
+
+ // When
+ var pendingOrders = orderRepository.findByStatus("PENDING");
+
+ // Then - Using sequenced collection methods
+ assertThat(pendingOrders).hasSize(1);
+ assertThat(pendingOrders.getFirst().getStatus()).isEqualTo("PENDING");
+}
+```
+
+## Testing Native Queries
+
+```java
+@Test
+void shouldExecuteNativeQuery() {
+ entityManager.persist(new Order("PENDING", BigDecimal.valueOf(100)));
+ entityManager.persist(new Order("PENDING", BigDecimal.valueOf(200)));
+ entityManager.flush();
+
+ var total = orderRepository.calculatePendingTotal();
+
+ assertThat(total).isEqualTo(new BigDecimal("300.00"));
+}
+```
+
+## Testing Pagination
+
+```java
+@Test
+void shouldReturnPagedResults() {
+ // Insert 20 orders using IntStream
+ IntStream.range(0, 20).forEach(i -> {
+ entityManager.persist(new Order("PENDING"));
+ });
+ entityManager.flush();
+
+ var page = orderRepository.findByStatus("PENDING", PageRequest.of(0, 10));
+
+ assertThat(page.getContent()).hasSize(10);
+ assertThat(page.getTotalElements()).isEqualTo(20);
+ assertThat(page.getContent().getFirst().getStatus()).isEqualTo("PENDING");
+}
+```
+
+## Testing Lazy Loading
+
+```java
+@Test
+void shouldLazyLoadOrderItems() {
+ var order = new Order("PENDING");
+ order.addItem(new OrderItem("Product", 2));
+ entityManager.persist(order);
+ entityManager.flush();
+ entityManager.clear(); // Detach from persistence context
+
+ var found = orderRepository.findById(order.getId());
+
+ assertThat(found).isPresent();
+ // This will trigger lazy loading
+ assertThat(found.get().getItems()).hasSize(1);
+ assertThat(found.get().getItems().getFirst().getProduct()).isEqualTo("Product");
+}
+```
+
+## Testing Cascading
+
+```java
+@Test
+void shouldCascadeDelete() {
+ var order = new Order("PENDING");
+ order.addItem(new OrderItem("Product", 2));
+ entityManager.persist(order);
+ entityManager.flush();
+
+ orderRepository.delete(order);
+ entityManager.flush();
+
+ assertThat(entityManager.find(OrderItem.class, order.getItems().getFirst().getId()))
+ .isNull();
+}
+```
+
+## Testing @Query Methods
+
+```java
+@Query("SELECT o FROM Order o WHERE o.createdAt > :date AND o.status = :status")
+List findRecentByStatus(@Param("date") LocalDateTime date,
+ @Param("status") String status);
+
+@Test
+void shouldFindRecentOrders() {
+ var old = new Order("PENDING");
+ old.setCreatedAt(LocalDateTime.now().minusDays(10));
+ var recent = new Order("PENDING");
+ recent.setCreatedAt(LocalDateTime.now().minusHours(1));
+
+ entityManager.persist(old);
+ entityManager.persist(recent);
+ entityManager.flush();
+
+ var recentOrders = orderRepository.findRecentByStatus(
+ LocalDateTime.now().minusDays(1), "PENDING");
+
+ assertThat(recentOrders).hasSize(1);
+ assertThat(recentOrders.getFirst().getId()).isEqualTo(recent.getId());
+}
+```
+
+## Using H2 vs Real Database
+
+### H2 (Default - Not Recommended for Production Parity)
+
+```java
+@DataJpaTest // Uses embedded H2 by default
+class OrderRepositoryH2Test {
+ // Fast but may miss DB-specific issues
+}
+```
+
+### Testcontainers (Recommended)
+
+```java
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+@Testcontainers
+class OrderRepositoryPostgresTest {
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:18");
+}
+```
+
+## Transaction Behavior
+
+Tests are @Transactional by default and roll back after each test.
+
+```java
+@Test
+@Rollback(false) // Don't roll back (rarely needed)
+void shouldPersistData() {
+ orderRepository.save(new Order("PENDING"));
+ // Data will remain in database after test
+}
+```
+
+## Key Points
+
+1. Use TestEntityManager for setup data
+2. Always flush() after persist() to trigger SQL
+3. Clear() the entity manager to test lazy loading
+4. Use real database (Testcontainers) for accurate results
+5. Test both success and failure cases
+6. Leverage Java 25 var keyword for cleaner variable declarations
+7. Use sequenced collection methods (getFirst(), getLast(), reversed())
diff --git a/skills/spring-boot-testing/references/instancio.md b/skills/spring-boot-testing/references/instancio.md
new file mode 100644
index 00000000..ce18aa2a
--- /dev/null
+++ b/skills/spring-boot-testing/references/instancio.md
@@ -0,0 +1,230 @@
+# Instancio
+
+Generate complex test objects automatically. Use when entities/DTOs have 3+ properties.
+
+## When to Use
+
+- Objects with **3 or more properties**
+- Setting up test data for repositories
+- Creating DTOs for controller tests
+- Avoiding repetitive builder/setter calls
+
+## Dependency
+
+```xml
+
+ org.instancio
+ instancio-junit
+ 5.5.1
+ test
+
+```
+
+## Basic Usage
+
+### Simple Object
+
+```java
+final var order = Instancio.create(Order.class);
+// All fields populated with random data
+```
+
+### List of Objects
+
+```java
+final var orders = Instancio.ofList(Order.class).size(5).create();
+// 5 orders with random data
+```
+
+## Customizing Values
+
+### Set Specific Fields
+
+```java
+final var order = Instancio.of(Order.class)
+ .set(field(Order::getStatus), "PENDING")
+ .set(field(Order::getTotal), new BigDecimal("99.99"))
+ .create();
+```
+
+### Supply Generated Values
+
+```java
+final var order = Instancio.of(Order.class)
+ .supply(field(Order::getEmail), () -> "user" + UUID.randomUUID() + "@test.com")
+ .create();
+```
+
+### Ignore Fields
+
+```java
+final var order = Instancio.of(Order.class)
+ .ignore(field(Order::getId)) // Let DB generate
+ .create();
+```
+
+## Complex Objects
+
+### Nested Objects
+
+```java
+final var order = Instancio.of(Order.class)
+ .set(field(Order::getCustomer), Instancio.create(Customer.class))
+ .set(field(Order::getItems), Instancio.ofList(OrderItem.class).size(3).create())
+ .create();
+```
+
+### All Fields Random
+
+```java
+// When you need fully random but valid data
+final var randomOrder = Instancio.create(Order.class);
+// Customer, items, addresses - all populated
+```
+
+## Spring Boot Integration
+
+### Repository Test Setup
+
+```java
+@DataJpaTest
+@AutoConfigureTestDatabase
+@Testcontainers
+class OrderRepositoryTest {
+
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:18");
+
+ @Autowired
+ private OrderRepository orderRepository;
+
+ @Test
+ void shouldFindOrdersByStatus() {
+ // Given: Create 10 random orders with PENDING status
+ final var orders = Instancio.ofList(Order.class)
+ .size(10)
+ .set(field(Order::getStatus), "PENDING")
+ .create();
+
+ orderRepository.saveAll(orders);
+
+ // When
+ final var found = orderRepository.findByStatus("PENDING");
+
+ // Then
+ assertThat(found).hasSize(10);
+ }
+}
+```
+
+### Controller Test Setup
+
+```java
+@WebMvcTest(OrderController.class)
+class OrderControllerTest {
+
+ @Autowired
+ private MockMvcTester mvc;
+
+ @MockitoBean
+ private OrderService orderService;
+
+ @Test
+ void shouldReturnOrder() {
+ // Given: Random order with specific ID
+ Order order = Instancio.of(Order.class)
+ .set(field(Order::getId), 1L)
+ .create();
+
+ given(orderService.findById(1L)).willReturn(order);
+
+ // When/Then
+ assertThat(mvc.get().uri("/orders/1"))
+ .hasStatus(HttpStatus.OK)
+ .bodyJson()
+ .convertTo(OrderResponse.class)
+ .satisfies(response -> {
+ assertThat(response.getId()).isEqualTo(1L);
+ });
+ }
+}
+```
+
+## Patterns
+
+### Builder Pattern Alternative
+
+```java
+// Instead of:
+Order order = Order.builder()
+ .id(1L)
+ .status("PENDING")
+ .customer(Customer.builder().name("John").build())
+ .items(List.of(
+ OrderItem.builder().product("A").price(10).build(),
+ OrderItem.builder().product("B").price(20).build()
+ ))
+ .build();
+
+// Use:
+Order order = Instancio.of(Order.class)
+ .set(field(Order::getId), 1L)
+ .set(field(Order::getStatus), "PENDING")
+ .create();
+// Customer and items auto-generated
+```
+
+### Seeded Data
+
+```java
+// Consistent "random" data for reproducible tests
+Order order = Instancio.of(Order.class)
+ .withSeed(12345L)
+ .create();
+// Same data every test run with seed 12345
+```
+
+## Common Patterns
+
+### Email Generation
+
+```java
+String email = Instancio.gen().net().email();
+```
+
+### Date Generation
+
+```java
+LocalDateTime createdAt = Instancio.gen().temporal()
+ .localDateTime()
+ .past()
+ .create();
+```
+
+### String Patterns
+
+```java
+String phone = Instancio.gen().text().pattern("+1-###-###-####");
+```
+
+## Comparison
+
+| Approach | Lines of Code | Maintainability |
+| -------- | ------------- | --------------- |
+| Manual setters | 10-20 | Low |
+| Builder pattern | 5-10 | Medium |
+| **Instancio** | 2-5 | **High** |
+
+## Best Practices
+
+1. **Use for 3+ property objects** - Not worth it for simple objects
+2. **Set only what's relevant** - Let Instancio fill the rest
+3. **Use with Testcontainers** - Great for database seeding
+4. **Set IDs explicitly** - When testing specific scenarios
+5. **Ignore auto-generated fields** - Like createdAt, updatedAt
+
+## Links
+
+- [Instancio Documentation](https://www.instancio.org/)
+- [JUnit 5 Extension](https://www.instancio.org/user-guide/#junit-integration)
diff --git a/skills/spring-boot-testing/references/mockitobean.md b/skills/spring-boot-testing/references/mockitobean.md
new file mode 100644
index 00000000..16797812
--- /dev/null
+++ b/skills/spring-boot-testing/references/mockitobean.md
@@ -0,0 +1,232 @@
+# @MockitoBean
+
+Mocking dependencies in Spring Boot tests (replaces deprecated @MockBean in Spring Boot 4+).
+
+## Overview
+
+`@MockitoBean` replaces the deprecated `@MockBean` annotation in Spring Boot 4.0+. It creates a Mockito mock and registers it in the Spring context, replacing any existing bean of the same type.
+
+## Basic Usage
+
+```java
+@WebMvcTest(OrderController.class)
+class OrderControllerTest {
+
+ @MockitoBean
+ private OrderService orderService;
+
+ @MockitoBean
+ private UserService userService;
+}
+```
+
+## Supported Test Slices
+
+- `@WebMvcTest` - Mock service/repository dependencies
+- `@WebFluxTest` - Mock reactive service dependencies
+- `@SpringBootTest` - Replace real beans with mocks
+
+## Stubbing Methods
+
+### Basic Stub
+
+```java
+@Test
+void shouldReturnOrder() {
+ Order order = new Order(1L, "PENDING");
+ given(orderService.findById(1L)).willReturn(order);
+
+ // Test code
+}
+```
+
+### Multiple Returns
+
+```java
+given(orderService.findById(anyLong()))
+ .willReturn(new Order(1L, "PENDING"))
+ .willReturn(new Order(2L, "COMPLETED"));
+```
+
+### Throwing Exceptions
+
+```java
+given(orderService.findById(999L))
+ .willThrow(new OrderNotFoundException(999L));
+```
+
+### Argument Matching
+
+```java
+given(orderService.create(argThat(req -> req.getQuantity() > 0)))
+ .willReturn(1L);
+
+given(orderService.findByStatus(eq("PENDING")))
+ .willReturn(List.of(new Order()));
+```
+
+## Verifying Interactions
+
+### Verify Method Called
+
+```java
+verify(orderService).findById(1L);
+```
+
+### Verify Never Called
+
+```java
+verify(orderService, never()).delete(any());
+```
+
+### Verify Count
+
+```java
+verify(orderService, times(2)).findById(anyLong());
+verify(orderService, atLeastOnce()).findByStatus(anyString());
+```
+
+### Verify Order
+
+```java
+InOrder inOrder = inOrder(orderService, userService);
+inOrder.verify(orderService).findById(1L);
+inOrder.verify(userService).getUser(any());
+```
+
+## Resetting Mocks
+
+Mocks are reset between tests automatically. To reset mid-test:
+
+```java
+Mockito.reset(orderService);
+```
+
+## @MockitoSpyBean for Partial Mocking
+
+Use `@MockitoSpyBean` to wrap a real bean with Mockito.
+
+```java
+@SpringBootTest
+class OrderServiceIntegrationTest {
+
+ @MockitoSpyBean
+ private PaymentGatewayClient paymentClient;
+
+ @Test
+ void shouldProcessOrder() {
+ doReturn(true).when(paymentClient).processPayment(any());
+
+ // Test with real service but mocked payment client
+ }
+}
+```
+
+## @TestBean for Custom Test Beans
+
+Register a custom bean instance in the test context:
+
+```java
+@SpringBootTest
+class OrderServiceTest {
+
+ @TestBean
+ private PaymentGatewayClient paymentClient() {
+ return new FakePaymentClient();
+ }
+}
+```
+
+## Scoping: Singleton vs Prototype
+
+Spring Framework 7+ (Spring Boot 4+) supports mocking non-singleton beans:
+
+```java
+@Component
+@Scope("prototype")
+public class OrderProcessor {
+ public String process() { return "real"; }
+}
+
+@SpringBootTest
+class OrderServiceTest {
+ @MockitoBean
+ private OrderProcessor orderProcessor;
+
+ @Test
+ void shouldWorkWithPrototype() {
+ given(orderProcessor.process()).willReturn("mocked");
+ // Test code
+ }
+}
+```
+
+## Common Patterns
+
+### Mocking Repository in Service Test
+
+```java
+@SpringBootTest
+class OrderServiceTest {
+ @MockitoBean
+ private OrderRepository orderRepository;
+
+ @Autowired
+ private OrderService orderService;
+
+ @Test
+ void shouldCreateOrder() {
+ given(orderRepository.save(any())).willReturn(new Order(1L));
+
+ Long id = orderService.createOrder(new OrderRequest());
+
+ assertThat(id).isEqualTo(1L);
+ verify(orderRepository).save(any(Order.class));
+ }
+}
+```
+
+### Multiple Mocks of Same Type
+
+Use bean names:
+
+```java
+@MockitoBean(name = "primaryDataSource")
+private DataSource primaryDataSource;
+
+@MockitoBean(name = "secondaryDataSource")
+private DataSource secondaryDataSource;
+```
+
+## Migration from @MockBean
+
+### Before (Deprecated)
+
+```java
+@MockBean
+private OrderService orderService;
+```
+
+### After (Spring Boot 4+)
+
+```java
+@MockitoBean
+private OrderService orderService;
+```
+
+## Key Differences from Mockito @Mock
+
+| Feature | @MockitoBean | @Mock |
+| ------- | ------------ | ----- |
+| Context integration | Yes | No |
+| Spring lifecycle | Participates | None |
+| Works with @Autowired | Yes | No |
+| Test slice support | Yes | Limited |
+
+## Best Practices
+
+1. Use `@MockitoBean` only when Spring context is involved
+2. For pure unit tests, use Mockito's `@Mock` or `Mockito.mock()`
+3. Always verify interactions that have side effects
+4. Don't verify simple queries (stubbing is enough)
+5. Reset mocks if test modifies shared mock state
diff --git a/skills/spring-boot-testing/references/mockmvc-classic.md b/skills/spring-boot-testing/references/mockmvc-classic.md
new file mode 100644
index 00000000..c490f98c
--- /dev/null
+++ b/skills/spring-boot-testing/references/mockmvc-classic.md
@@ -0,0 +1,206 @@
+# MockMvc Classic
+
+Traditional MockMvc API for Spring MVC controller tests (pre-Spring Boot 3.2 or legacy codebases).
+
+## When to Use This Reference
+
+- The project uses Spring Boot < 3.2 (no `MockMvcTester` available)
+- Existing tests use `mvc.perform(...)` and you are maintaining or extending them
+- You need to migrate classic MockMvc tests to `MockMvcTester` (see migration section below)
+- The user explicitly asks about `ResultActions`, `andExpect()`, or Hamcrest-style web assertions
+
+For new tests on Spring Boot 3.2+, prefer [mockmvc-tester.md](mockmvc-tester.md) instead.
+
+## Setup
+
+```java
+@WebMvcTest(OrderController.class)
+class OrderControllerTest {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @MockBean
+ private OrderService orderService;
+}
+```
+
+## Basic GET Request
+
+```java
+@Test
+void shouldReturnOrder() throws Exception {
+ given(orderService.findById(1L)).willReturn(new Order(1L, "PENDING", 99.99));
+
+ mvc.perform(get("/orders/1"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$.id").value(1))
+ .andExpect(jsonPath("$.status").value("PENDING"))
+ .andExpect(jsonPath("$.totalToPay").value(99.99));
+}
+```
+
+## POST with Request Body
+
+```java
+@Test
+void shouldCreateOrder() throws Exception {
+ given(orderService.create(any(OrderRequest.class))).willReturn(1L);
+
+ mvc.perform(post("/orders")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"product\": \"Laptop\", \"quantity\": 2}"))
+ .andExpect(status().isCreated())
+ .andExpect(header().string("Location", "/orders/1"));
+}
+```
+
+## PUT Request
+
+```java
+@Test
+void shouldUpdateOrder() throws Exception {
+ mvc.perform(put("/orders/1")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"status\": \"COMPLETED\"}"))
+ .andExpect(status().isOk());
+}
+```
+
+## DELETE Request
+
+```java
+@Test
+void shouldDeleteOrder() throws Exception {
+ mvc.perform(delete("/orders/1"))
+ .andExpect(status().isNoContent());
+}
+```
+
+## Status Matchers
+
+```java
+.andExpect(status().isOk()) // 200
+.andExpect(status().isCreated()) // 201
+.andExpect(status().isNoContent()) // 204
+.andExpect(status().isBadRequest()) // 400
+.andExpect(status().isUnauthorized()) // 401
+.andExpect(status().isForbidden()) // 403
+.andExpect(status().isNotFound()) // 404
+.andExpect(status().is(422)) // arbitrary code
+```
+
+## JSON Path Assertions
+
+```java
+// Exact value
+.andExpect(jsonPath("$.status").value("PENDING"))
+
+// Existence
+.andExpect(jsonPath("$.id").exists())
+.andExpect(jsonPath("$.deletedAt").doesNotExist())
+
+// Array size
+.andExpect(jsonPath("$.items").isArray())
+.andExpect(jsonPath("$.items", hasSize(3)))
+
+// Nested field
+.andExpect(jsonPath("$.customer.name").value("John Doe"))
+.andExpect(jsonPath("$.customer.address.city").value("Berlin"))
+
+// With Hamcrest matchers
+.andExpect(jsonPath("$.total", greaterThan(0.0)))
+.andExpect(jsonPath("$.description", containsString("order")))
+```
+
+## Content Assertions
+
+```java
+.andExpect(content().contentType(MediaType.APPLICATION_JSON))
+.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+.andExpect(content().string(containsString("PENDING")))
+.andExpect(content().json("{\"status\":\"PENDING\"}"))
+```
+
+## Header Assertions
+
+```java
+.andExpect(header().string("Location", "/orders/1"))
+.andExpect(header().string("Content-Type", containsString("application/json")))
+.andExpect(header().exists("X-Request-Id"))
+.andExpect(header().doesNotExist("X-Deprecated"))
+```
+
+## Request Parameters and Headers
+
+```java
+// Query parameters
+mvc.perform(get("/orders").param("status", "PENDING").param("page", "0"))
+ .andExpect(status().isOk());
+
+// Path variables
+mvc.perform(get("/orders/{id}", 1L))
+ .andExpect(status().isOk());
+
+// Request headers
+mvc.perform(get("/orders/1").header("X-Api-Key", "secret"))
+ .andExpect(status().isOk());
+```
+
+## Capturing the Response
+
+```java
+@Test
+void shouldReturnCreatedId() throws Exception {
+ given(orderService.create(any())).willReturn(42L);
+
+ MvcResult result = mvc.perform(post("/orders")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"product\": \"Laptop\", \"quantity\": 1}"))
+ .andExpect(status().isCreated())
+ .andReturn();
+
+ String location = result.getResponse().getHeader("Location");
+ assertThat(location).isEqualTo("/orders/42");
+}
+```
+
+## Chaining with andDo
+
+```java
+mvc.perform(get("/orders/1"))
+ .andDo(print()) // prints request/response to console (debug)
+ .andExpect(status().isOk());
+```
+
+## Static Imports
+
+```java
+import org.springframework.boot.test.mock.mockito.MockBean;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
+import static org.hamcrest.Matchers.*;
+```
+
+## Migration to MockMvcTester
+
+| Classic MockMvc | MockMvcTester (recommended) |
+| --- | --- |
+| `@Autowired MockMvc mvc` | `@Autowired MockMvcTester mvc` |
+| `mvc.perform(get("/orders/1"))` | `mvc.get().uri("/orders/1")` |
+| `.andExpect(status().isOk())` | `.hasStatusOk()` |
+| `.andExpect(jsonPath("$.status").value("X"))` | `.bodyJson().convertTo(T.class)` + AssertJ |
+| `throws Exception` on every method | No checked exception |
+| Hamcrest matchers | AssertJ fluent assertions |
+
+See [mockmvc-tester.md](mockmvc-tester.md) for the full modern API.
+
+## Key Points
+
+1. **Every test method must declare `throws Exception`** — `perform()` throws checked exceptions
+2. **Use `andDo(print())` during debugging** — remove before committing
+3. **Prefer `jsonPath()` over `content().string()`** — more precise field-level assertions
+4. **Static imports are required** — IDE can auto-add them
+5. **Migrate to MockMvcTester** when upgrading to Spring Boot 3.2+ for better readability
diff --git a/skills/spring-boot-testing/references/mockmvc-tester.md b/skills/spring-boot-testing/references/mockmvc-tester.md
new file mode 100644
index 00000000..87a19be0
--- /dev/null
+++ b/skills/spring-boot-testing/references/mockmvc-tester.md
@@ -0,0 +1,311 @@
+# MockMvcTester
+
+AssertJ-style testing for Spring MVC controllers (Spring Boot 3.2+).
+
+## Overview
+
+MockMvcTester provides fluent, AssertJ-style assertions for web layer testing. More readable and type-safe than traditional MockMvc.
+
+**Recommended Pattern**: Convert JSON to real objects and assert with AssertJ:
+
+```java
+assertThat(mvc.get().uri("/orders/1"))
+ .hasStatus(HttpStatus.OK)
+ .bodyJson()
+ .convertTo(OrderResponse.class)
+ .satisfies(response -> {
+ assertThat(response.getTotalToPay()).isEqualTo(expectedAmount);
+ assertThat(response.getItems()).isNotEmpty();
+ });
+```
+
+## Basic Usage
+
+```java
+@WebMvcTest(OrderController.class)
+class OrderControllerTest {
+
+ @Autowired
+ private MockMvcTester mvc;
+
+ @MockitoBean
+ private OrderService orderService;
+}
+```
+
+## Recommended: Object Conversion Pattern
+
+### Single Object Response
+
+```java
+@Test
+void shouldGetOrder() {
+ given(orderService.findById(1L)).willReturn(new Order(1L, "PENDING", 99.99));
+
+ assertThat(mvc.get().uri("/orders/1"))
+ .hasStatus(HttpStatus.OK)
+ .bodyJson()
+ .convertTo(OrderResponse.class)
+ .satisfies(response -> {
+ assertThat(response.getId()).isEqualTo(1L);
+ assertThat(response.getStatus()).isEqualTo("PENDING");
+ assertThat(response.getTotalToPay()).isEqualTo(new BigDecimal("99.99"));
+ });
+}
+```
+
+### List Response
+
+```java
+@Test
+void shouldGetAllOrders() {
+ given(orderService.findAll()).willReturn(Arrays.asList(
+ new Order(1L, "PENDING"),
+ new Order(2L, "COMPLETED")
+ ));
+
+ assertThat(mvc.get().uri("/orders"))
+ .hasStatus(HttpStatus.OK)
+ .bodyJson()
+ .convertTo(new TypeReference>() {})
+ .satisfies(orders -> {
+ assertThat(orders).hasSize(2);
+ assertThat(orders.get(0).getStatus()).isEqualTo("PENDING");
+ assertThat(orders.get(1).getStatus()).isEqualTo("COMPLETED");
+ });
+}
+```
+
+### Nested Objects
+
+```java
+@Test
+void shouldGetOrderWithCustomer() {
+ assertThat(mvc.get().uri("/orders/1"))
+ .hasStatus(HttpStatus.OK)
+ .bodyJson()
+ .convertTo(OrderResponse.class)
+ .satisfies(response -> {
+ assertThat(response.getCustomer()).isNotNull();
+ assertThat(response.getCustomer().getName()).isEqualTo("John Doe");
+ assertThat(response.getCustomer().getAddress().getCity()).isEqualTo("Berlin");
+ });
+}
+```
+
+### Complex Assertions
+
+```java
+@Test
+void shouldCalculateOrderTotal() {
+ assertThat(mvc.get().uri("/orders/1/calculate"))
+ .hasStatus(HttpStatus.OK)
+ .bodyJson()
+ .convertTo(CalculationResponse.class)
+ .satisfies(calc -> {
+ assertThat(calc.getSubtotal()).isEqualTo(new BigDecimal("100.00"));
+ assertThat(calc.getTax()).isEqualTo(new BigDecimal("19.00"));
+ assertThat(calc.getTotalToPay()).isEqualTo(new BigDecimal("119.00"));
+ assertThat(calc.getItems()).allMatch(item -> item.getPrice().compareTo(BigDecimal.ZERO) > 0);
+ });
+}
+```
+
+## HTTP Methods
+
+### POST with Request Body
+
+```java
+@Test
+void shouldCreateOrder() {
+ given(orderService.create(any())).willReturn(1L);
+
+ assertThat(mvc.post().uri("/orders")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"product\": \"Laptop\", \"quantity\": 2}"))
+ .hasStatus(HttpStatus.CREATED)
+ .hasHeader("Location", "/orders/1");
+}
+```
+
+### PUT Request
+
+```java
+@Test
+void shouldUpdateOrder() {
+ assertThat(mvc.put().uri("/orders/1")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"status\": \"COMPLETED\"}"))
+ .hasStatus(HttpStatus.OK);
+}
+```
+
+### DELETE Request
+
+```java
+@Test
+void shouldDeleteOrder() {
+ assertThat(mvc.delete().uri("/orders/1"))
+ .hasStatus(HttpStatus.NO_CONTENT);
+}
+```
+
+## Status Assertions
+
+```java
+assertThat(mvc.get().uri("/orders/1"))
+ .hasStatusOk() // 200
+ .hasStatus(HttpStatus.OK) // 200
+ .hasStatus2xxSuccessful() // 2xx
+ .hasStatusBadRequest() // 400
+ .hasStatusNotFound() // 404
+ .hasStatusUnauthorized() // 401
+ .hasStatusForbidden() // 403
+ .hasStatus(HttpStatus.CREATED); // 201
+```
+
+## Content Type Assertions
+
+```java
+assertThat(mvc.get().uri("/orders/1"))
+ .hasContentType(MediaType.APPLICATION_JSON)
+ .hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON);
+```
+
+## Header Assertions
+
+```java
+assertThat(mvc.post().uri("/orders"))
+ .hasHeader("Location", "/orders/123")
+ .hasHeader("X-Request-Id", matchesPattern("[a-z0-9-]+"));
+```
+
+## Alternative: JSON Path (Use Sparingly)
+
+Only use when you cannot convert to a typed object:
+
+```java
+assertThat(mvc.get().uri("/orders/1"))
+ .hasStatusOk()
+ .bodyJson()
+ .extractingPath("$.customer.address.city")
+ .asString()
+ .isEqualTo("Berlin");
+```
+
+## Request Parameters
+
+```java
+// Query parameters
+assertThat(mvc.get().uri("/orders?status=PENDING&page=0"))
+ .hasStatusOk();
+
+// Path parameters
+assertThat(mvc.get().uri("/orders/{id}", 1L))
+ .hasStatusOk();
+
+// Headers
+assertThat(mvc.get().uri("/orders/1")
+ .header("X-Api-Key", "secret"))
+ .hasStatusOk();
+```
+
+## Request Body with JacksonTester
+
+```java
+@Autowired
+private JacksonTester json;
+
+@Test
+void shouldCreateOrder() {
+ OrderRequest request = new OrderRequest("Laptop", 2);
+
+ assertThat(mvc.post().uri("/orders")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(json.write(request).getJson()))
+ .hasStatus(HttpStatus.CREATED);
+}
+```
+
+## Error Responses
+
+```java
+@Test
+void shouldReturnValidationErrors() {
+ given(orderService.findById(999L))
+ .willThrow(new OrderNotFoundException(999L));
+
+ assertThat(mvc.get().uri("/orders/999"))
+ .hasStatus(HttpStatus.NOT_FOUND)
+ .bodyJson()
+ .convertTo(ErrorResponse.class)
+ .satisfies(error -> {
+ assertThat(error.getMessage()).isEqualTo("Order 999 not found");
+ assertThat(error.getCode()).isEqualTo("ORDER_NOT_FOUND");
+ });
+}
+```
+
+## Validation Error Testing
+
+```java
+@Test
+void shouldRejectInvalidOrder() {
+ OrderRequest invalidRequest = new OrderRequest("", -1);
+
+ assertThat(mvc.post().uri("/orders")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(json.write(invalidRequest).getJson()))
+ .hasStatus(HttpStatus.BAD_REQUEST)
+ .bodyJson()
+ .convertTo(ValidationErrorResponse.class)
+ .satisfies(errors -> {
+ assertThat(errors.getFieldErrors()).hasSize(2);
+ assertThat(errors.getFieldErrors())
+ .extracting("field")
+ .contains("product", "quantity");
+ });
+}
+```
+
+## Comparison: MockMvcTester vs Classic MockMvc
+
+| Feature | MockMvcTester | Classic MockMvc |
+| ------- | ------------- | --------------- |
+| Style | AssertJ fluent | MockMvc matchers |
+| Readability | High | Medium |
+| Type Safety | Better | Less |
+| IDE Support | Excellent | Good |
+| Object Conversion | Native | Manual |
+
+## Migration from Classic MockMvc
+
+### Before (Classic)
+
+```java
+mvc.perform(get("/orders/1"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value("PENDING"))
+ .andExpect(jsonPath("$.totalToPay").value(99.99));
+```
+
+### After (Tester with Object Conversion)
+
+```java
+assertThat(mvc.get().uri("/orders/1"))
+ .hasStatus(HttpStatus.OK)
+ .bodyJson()
+ .convertTo(OrderResponse.class)
+ .satisfies(response -> {
+ assertThat(response.getStatus()).isEqualTo("PENDING");
+ assertThat(response.getTotalToPay()).isEqualTo(new BigDecimal("99.99"));
+ });
+```
+
+## Key Points
+
+1. **Prefer `convertTo()` over `extractingPath()`** - Type-safe, refactorable
+2. **Use `satisfies()` for multiple assertions** - Keeps tests readable
+3. **Import static `org.assertj.core.api.Assertions.assertThat`**
+4. **Works with generics via `TypeReference`** - For `List` responses
+5. **IDE refactoring friendly** - Rename fields, IDE updates tests
diff --git a/skills/spring-boot-testing/references/restclienttest.md b/skills/spring-boot-testing/references/restclienttest.md
new file mode 100644
index 00000000..4d18808a
--- /dev/null
+++ b/skills/spring-boot-testing/references/restclienttest.md
@@ -0,0 +1,227 @@
+# @RestClientTest
+
+Testing REST clients in isolation with MockRestServiceServer.
+
+## Overview
+
+`@RestClientTest` auto-configures:
+
+- RestTemplate/RestClient with mock server support
+- Jackson ObjectMapper
+- MockRestServiceServer
+
+## Basic Setup
+
+```java
+@RestClientTest(WeatherService.class)
+class WeatherServiceTest {
+
+ @Autowired
+ private WeatherService weatherService;
+
+ @Autowired
+ private MockRestServiceServer server;
+}
+```
+
+## Testing RestTemplate
+
+```java
+@RestClientTest(WeatherService.class)
+class WeatherServiceTest {
+
+ @Autowired
+ private WeatherService weatherService;
+
+ @Autowired
+ private MockRestServiceServer server;
+
+ @Test
+ void shouldFetchWeather() {
+ // Given
+ server.expect(requestTo("https://api.weather.com/v1/current"))
+ .andExpect(method(HttpMethod.GET))
+ .andExpect(queryParam("city", "Berlin"))
+ .andRespond(withSuccess()
+ .contentType(MediaType.APPLICATION_JSON)
+ .body("{\"temperature\": 22, \"condition\": \"Sunny\"}"));
+
+ // When
+ Weather weather = weatherService.getCurrentWeather("Berlin");
+
+ // Then
+ assertThat(weather.getTemperature()).isEqualTo(22);
+ assertThat(weather.getCondition()).isEqualTo("Sunny");
+ }
+}
+```
+
+## Testing RestClient (Spring 6.1+)
+
+```java
+@RestClientTest(WeatherService.class)
+class WeatherServiceTest {
+
+ @Autowired
+ private WeatherService weatherService;
+
+ @Autowired
+ private MockRestServiceServer server;
+
+ @Test
+ void shouldFetchWeatherWithRestClient() {
+ server.expect(requestTo("https://api.weather.com/v1/current"))
+ .andRespond(withSuccess()
+ .body("{\"temperature\": 22}"));
+
+ Weather weather = weatherService.getCurrentWeather("Berlin");
+
+ assertThat(weather.getTemperature()).isEqualTo(22);
+ }
+}
+```
+
+## Request Matching
+
+### Exact URL
+
+```java
+server.expect(requestTo("https://api.example.com/users/1"))
+ .andRespond(withSuccess());
+```
+
+### URL Pattern
+
+```java
+server.expect(requestTo(matchesPattern("https://api.example.com/users/\\d+")))
+ .andRespond(withSuccess());
+```
+
+### HTTP Method
+
+```java
+server.expect(ExpectedCount.once(),
+ requestTo("https://api.example.com/users"))
+ .andExpect(method(HttpMethod.POST))
+ .andRespond(withCreatedEntity(URI.create("/users/1")));
+```
+
+### Request Body
+
+```java
+server.expect(requestTo("https://api.example.com/users"))
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(content().json("{\"name\": \"John\"}"))
+ .andRespond(withSuccess());
+```
+
+### Headers
+
+```java
+server.expect(requestTo("https://api.example.com/users"))
+ .andExpect(header("Authorization", "Bearer token123"))
+ .andExpect(header("X-Api-Key", "secret"))
+ .andRespond(withSuccess());
+```
+
+## Response Types
+
+### Success with Body
+
+```java
+server.expect(requestTo("/users/1"))
+ .andRespond(withSuccess()
+ .contentType(MediaType.APPLICATION_JSON)
+ .body("{\"id\": 1, \"name\": \"John\"}"));
+```
+
+### Success from Resource
+
+```java
+server.expect(requestTo("/users/1"))
+ .andRespond(withSuccess()
+ .body(new ClassPathResource("user-response.json")));
+```
+
+### Created
+
+```java
+server.expect(requestTo("/users"))
+ .andExpect(method(HttpMethod.POST))
+ .andRespond(withCreatedEntity(URI.create("/users/1")));
+```
+
+### Error Response
+
+```java
+server.expect(requestTo("/users/999"))
+ .andRespond(withResourceNotFound());
+
+server.expect(requestTo("/users"))
+ .andRespond(withServerError()
+ .body("Internal Server Error"));
+
+server.expect(requestTo("/users"))
+ .andRespond(withStatus(HttpStatus.BAD_REQUEST)
+ .body("{\"error\": \"Invalid input\"}"));
+```
+
+## Verifying Requests
+
+```java
+@Test
+void shouldCallApi() {
+ server.expect(ExpectedCount.once(),
+ requestTo("https://api.example.com/data"))
+ .andRespond(withSuccess());
+
+ service.fetchData();
+
+ server.verify(); // Verify all expectations met
+}
+```
+
+## Ignoring Extra Requests
+
+```java
+@Test
+void shouldHandleMultipleCalls() {
+ server.expect(ExpectedCount.manyTimes(),
+ requestTo(matchesPattern("/api/.*")))
+ .andRespond(withSuccess());
+
+ // Multiple calls allowed
+ service.callApi();
+ service.callApi();
+ service.callApi();
+}
+```
+
+## Reset Between Tests
+
+```java
+@BeforeEach
+void setUp() {
+ server.reset();
+}
+```
+
+## Testing Timeouts
+
+```java
+server.expect(requestTo("/slow-endpoint"))
+ .andRespond(withSuccess()
+ .body("{\"data\": \"test\"}")
+ .delay(100, TimeUnit.MILLISECONDS));
+
+// Test timeout handling
+```
+
+## Best Practices
+
+1. Always verify `server.verify()` at end of test
+2. Use resource files for large JSON responses
+3. Match on minimal set of request attributes
+4. Reset server in @BeforeEach
+5. Test error responses, not just success
+6. Verify request body for POST/PUT calls
diff --git a/skills/spring-boot-testing/references/resttestclient.md b/skills/spring-boot-testing/references/resttestclient.md
new file mode 100644
index 00000000..9fc34b56
--- /dev/null
+++ b/skills/spring-boot-testing/references/resttestclient.md
@@ -0,0 +1,278 @@
+# RestTestClient
+
+Modern REST client testing with Spring Boot 4+ (replaces TestRestTemplate).
+
+## Overview
+
+RestTestClient is the modern alternative to TestRestTemplate in Spring Boot 4.0+. It provides a fluent, reactive API for testing REST endpoints.
+
+## Setup
+
+### Dependency (Spring Boot 4+)
+
+```xml
+
+ org.springframework.boot
+ spring-boot-starter-restclient-test
+ test
+
+```
+
+### Basic Configuration
+
+```java
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+@AutoConfigureRestTestClient
+class OrderIntegrationTest {
+
+ @Autowired
+ private RestTestClient restClient;
+}
+```
+
+## HTTP Methods
+
+### GET Request
+
+```java
+@Test
+void shouldGetOrder() {
+ restClient
+ .get()
+ .uri("/orders/1")
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectBody(Order.class)
+ .value(order -> {
+ assertThat(order.getId()).isEqualTo(1L);
+ assertThat(order.getStatus()).isEqualTo("PENDING");
+ });
+}
+```
+
+### POST Request
+
+```java
+@Test
+void shouldCreateOrder() {
+ OrderRequest request = new OrderRequest("Laptop", 2);
+
+ restClient
+ .post()
+ .uri("/orders")
+ .contentType(MediaType.APPLICATION_JSON)
+ .body(request)
+ .exchange()
+ .expectStatus()
+ .isCreated()
+ .expectHeader()
+ .location("/orders/1")
+ .expectBody(Long.class)
+ .isEqualTo(1L);
+}
+```
+
+### PUT Request
+
+```java
+@Test
+void shouldUpdateOrder() {
+ restClient
+ .put()
+ .uri("/orders/1")
+ .body(new OrderUpdate("COMPLETED"))
+ .exchange()
+ .expectStatus()
+ .isOk();
+}
+```
+
+### DELETE Request
+
+```java
+@Test
+void shouldDeleteOrder() {
+ restClient
+ .delete()
+ .uri("/orders/1")
+ .exchange()
+ .expectStatus()
+ .isNoContent();
+}
+```
+
+## Response Assertions
+
+### Status Codes
+
+```java
+restClient
+ .get()
+ .uri("/orders/1")
+ .exchange()
+ .expectStatus()
+ .isOk() // 200
+ .isCreated() // 201
+ .isNoContent() // 204
+ .isBadRequest() // 400
+ .isNotFound() // 404
+ .is5xxServerError() // 5xx
+ .isEqualTo(200); // Specific code
+```
+
+### Headers
+
+```java
+restClient
+ .post()
+ .uri("/orders")
+ .exchange()
+ .expectHeader()
+ .location("/orders/1")
+ .contentType(MediaType.APPLICATION_JSON)
+ .exists("X-Request-Id")
+ .valueEquals("X-Api-Version", "v1");
+```
+
+### Body Assertions
+
+```java
+restClient
+ .get()
+ .uri("/orders/1")
+ .exchange()
+ .expectBody(Order.class)
+ .value(order -> assertThat(order.getId()).isEqualTo(1L))
+ .returnResult();
+```
+
+### JSON Path
+
+```java
+restClient
+ .get()
+ .uri("/orders")
+ .exchange()
+ .expectBody()
+ .jsonPath("$.content[0].id").isEqualTo(1)
+ .jsonPath("$.content[0].status").isEqualTo("PENDING")
+ .jsonPath("$.totalElements").isNumber();
+```
+
+## Request Configuration
+
+### Headers
+
+```java
+restClient
+ .get()
+ .uri("/orders/1")
+ .header("Authorization", "Bearer token")
+ .header("X-Api-Key", "secret")
+ .exchange();
+```
+
+### Query Parameters
+
+```java
+restClient
+ .get()
+ .uri(uriBuilder -> uriBuilder
+ .path("/orders")
+ .queryParam("status", "PENDING")
+ .queryParam("page", 0)
+ .queryParam("size", 10)
+ .build())
+ .exchange();
+```
+
+### Path Variables
+
+```java
+restClient
+ .get()
+ .uri("/orders/{id}", 1L)
+ .exchange();
+```
+
+## With MockMvc
+
+RestTestClient can also work with MockMvc (no server startup):
+
+```java
+@SpringBootTest
+@AutoConfigureMockMvc
+@AutoConfigureRestTestClient
+class OrderMockMvcTest {
+
+ @Autowired
+ private RestTestClient restClient;
+
+ @Test
+ void shouldWorkWithMockMvc() {
+ // Uses MockMvc under the hood - no server startup
+ restClient
+ .get()
+ .uri("/orders/1")
+ .exchange()
+ .expectStatus()
+ .isOk();
+ }
+}
+```
+
+## Comparison: RestTestClient vs TestRestTemplate
+
+| Feature | RestTestClient | TestRestTemplate |
+| ------- | -------------- | ---------------- |
+| Style | Fluent/reactive | Imperative |
+| Spring Boot | 4.0+ | All versions (deprecated in 4) |
+| Assertions | Built-in | Manual |
+| MockMvc support | Yes | No |
+| Async | Native | Requires extra handling |
+
+## Migration from TestRestTemplate
+
+### Before (Deprecated)
+
+```java
+@Autowired
+private TestRestTemplate restTemplate;
+
+@Test
+void shouldGetOrder() {
+ ResponseEntity response = restTemplate
+ .getForEntity("/orders/1", Order.class);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+ assertThat(response.getBody().getId()).isEqualTo(1L);
+}
+```
+
+### After (RestTestClient)
+
+```java
+@Autowired
+private RestTestClient restClient;
+
+@Test
+void shouldGetOrder() {
+ restClient
+ .get()
+ .uri("/orders/1")
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectBody(Order.class)
+ .value(order -> assertThat(order.getId()).isEqualTo(1L));
+}
+```
+
+## Best Practices
+
+1. Use with @SpringBootTest(WebEnvironment.RANDOM_PORT) for real HTTP
+2. Use with @AutoConfigureMockMvc for faster tests without server
+3. Leverage fluent assertions for readability
+4. Test both success and error scenarios
+5. Verify headers for security/API versioning
diff --git a/skills/spring-boot-testing/references/sb4-migration.md b/skills/spring-boot-testing/references/sb4-migration.md
new file mode 100644
index 00000000..e4a42fc4
--- /dev/null
+++ b/skills/spring-boot-testing/references/sb4-migration.md
@@ -0,0 +1,181 @@
+# Spring Boot 4.0 Migration
+
+Key testing changes when migrating from Spring Boot 3.x to 4.0.
+
+## Dependency Changes
+
+### Modular Test Starters
+
+Spring Boot 4.0 introduces modular test starters:
+
+**Before (3.x):**
+
+```xml
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+```
+
+**After (4.0) - WebMvc Testing:**
+
+```xml
+
+ org.springframework.boot
+ spring-boot-starter-webmvc-test
+ test
+
+```
+
+**After (4.0) - REST Client Testing:**
+
+```xml
+
+ org.springframework.boot
+ spring-boot-starter-restclient-test
+ test
+
+```
+
+## Annotation Migration
+
+### @MockBean → @MockitoBean
+
+**Deprecated (3.x):**
+
+```java
+@MockBean
+private OrderService orderService;
+```
+
+**New (4.0):**
+
+```java
+@MockitoBean
+private OrderService orderService;
+```
+
+### @SpyBean → @MockitoSpyBean
+
+**Deprecated (3.x):**
+
+```java
+@SpyBean
+private PaymentGatewayClient paymentClient;
+```
+
+**New (4.0):**
+
+```java
+@MockitoSpyBean
+private PaymentGatewayClient paymentClient;
+```
+
+## New Testing Features
+
+### RestTestClient
+
+Replaces TestRestTemplate (deprecated):
+
+```java
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+@AutoConfigureRestTestClient
+class OrderIntegrationTest {
+
+ @Autowired
+ private RestTestClient restClient;
+
+ @Test
+ void shouldCreateOrder() {
+ restClient
+ .post()
+ .uri("/orders")
+ .body(new OrderRequest("Product", 2))
+ .exchange()
+ .expectStatus()
+ .isCreated()
+ .expectHeader()
+ .location("/orders/1");
+ }
+}
+```
+
+## JUnit 6 Support
+
+Spring Boot 4.0 uses JUnit 6 by default:
+
+- JUnit 4 is deprecated (use JUnit Vintage temporarily)
+- All JUnit 5 features still work
+- Remove JUnit 4 dependencies for clean migration
+
+## Testcontainers 2.0
+
+Module naming changed:
+
+**Before (1.x):**
+
+```xml
+postgresql
+```
+
+**After (2.0):**
+
+```xml
+testcontainers-postgresql
+```
+
+## Non-Singleton Bean Mocking
+
+Spring Framework 7 allows mocking prototype-scoped beans:
+
+```java
+@Component
+@Scope("prototype")
+public class OrderProcessor { }
+
+@SpringBootTest
+class OrderServiceTest {
+ @MockitoBean
+ private OrderProcessor orderProcessor; // Now works!
+}
+```
+
+## SpringExtension Context Changes
+
+Extension context is now test-method scoped by default.
+
+If tests fail with @Nested classes:
+
+```java
+@SpringExtensionConfig(useTestClassScopedExtensionContext = true)
+@SpringBootTest
+class OrderTest {
+ // Use old behavior
+}
+```
+
+## Migration Checklist
+
+- [ ] Replace @MockBean with @MockitoBean
+- [ ] Replace @SpyBean with @MockitoSpyBean
+- [ ] Update Testcontainers dependencies to 2.0 naming
+- [ ] Add modular test starters as needed
+- [ ] Migrate TestRestTemplate to RestTestClient
+- [ ] Remove JUnit 4 dependencies
+- [ ] Update custom TestExecutionListener implementations
+- [ ] Test @Nested class behavior
+
+## Backward Compatibility
+
+Use "classic" starters for gradual migration:
+
+```xml
+
+ org.springframework.boot
+ spring-boot-starter-test-classic
+ test
+
+```
+
+This provides old behavior while you migrate incrementally.
diff --git a/skills/spring-boot-testing/references/test-slices-overview.md b/skills/spring-boot-testing/references/test-slices-overview.md
new file mode 100644
index 00000000..892e1e1f
--- /dev/null
+++ b/skills/spring-boot-testing/references/test-slices-overview.md
@@ -0,0 +1,203 @@
+# Test Slices Overview
+
+Quick reference for selecting the right Spring Boot test slice.
+
+## Decision Matrix
+
+| Annotation | Use When | Loads | Speed |
+| ---------- | -------- | ----- | ----- |
+| **None** (plain JUnit) | Testing pure business logic | Nothing | Fastest |
+| `@WebMvcTest` | Controller + HTTP layer | Controllers, MVC, Jackson | Fast |
+| `@DataJpaTest` | Repository queries | Repositories, JPA, DataSource | Fast |
+| `@RestClientTest` | REST client code | RestTemplate/RestClient, Jackson | Fast |
+| `@JsonTest` | JSON serialization | ObjectMapper only | Fastest slice |
+| `@WebFluxTest` | Reactive controllers | Controllers, WebFlux | Fast |
+| `@DataJdbcTest` | JDBC repositories | Repositories, JDBC | Fast |
+| `@DataMongoTest` | MongoDB repositories | Repositories, MongoDB | Fast |
+| `@DataRedisTest` | Redis repositories | Repositories, Redis | Fast |
+| `@SpringBootTest` | Full integration | Entire application | Slow |
+
+## Selection Guide
+
+### Use NO Annotation (Plain Unit Test)
+
+```java
+class PriceCalculatorTest {
+ private PriceCalculator calculator = new PriceCalculator();
+
+ @Test
+ void shouldApplyDiscount() {
+ var result = calculator.applyDiscount(100, 0.1);
+ assertThat(result).isEqualTo(new BigDecimal("90.00"));
+ }
+}
+```
+
+**When**: Pure business logic, no dependencies or simple dependencies mockable via constructor injection.
+
+### Use @WebMvcTest
+
+```java
+@WebMvcTest(OrderController.class)
+class OrderControllerTest {
+ @Autowired private MockMvcTester mvc;
+ @MockitoBean private OrderService orderService;
+}
+```
+
+**When**: Testing request mapping, validation, JSON mapping, security, filters.
+
+**What you get**: MockMvc, ObjectMapper, Spring Security (if present), exception handlers.
+
+### Use @DataJpaTest
+
+```java
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+@Testcontainers
+class OrderRepositoryTest {
+ @Container
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:18");
+}
+```
+
+**When**: Testing custom JPA queries, entity mappings, transaction behavior, cascade operations.
+
+**What you get**: Repository beans, EntityManager, TestEntityManager, transaction support.
+
+### Use @RestClientTest
+
+```java
+@RestClientTest(WeatherService.class)
+class WeatherServiceTest {
+ @Autowired private WeatherService weatherService;
+ @Autowired private MockRestServiceServer server;
+}
+```
+
+**When**: Testing REST clients that call external APIs.
+
+**What you get**: MockRestServiceServer to stub HTTP responses.
+
+### Use @JsonTest
+
+```java
+@JsonTest
+class OrderJsonTest {
+ @Autowired private JacksonTester json;
+}
+```
+
+**When**: Testing custom serializers/deserializers, complex JSON mapping.
+
+### Use @SpringBootTest
+
+```java
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+@AutoConfigureRestTestClient
+class OrderIntegrationTest {
+ @Autowired private RestTestClient restClient;
+}
+```
+
+**When**: Testing full request flow, security filters, database interactions together.
+
+**What you get**: Full application context, embedded server (optional), real beans.
+
+## Common Mistakes
+
+1. **Using @SpringBootTest for everything** - Slows down your test suite unnecessarily
+2. **@WebMvcTest without mocking services** - Causes context loading failures
+3. **@DataJpaTest with @MockBean** - Defeats the purpose (you want real repositories)
+4. **Multiple slices in one test** - Each slice is a separate test class
+
+## Java 25 Features in Tests
+
+### Records for Test Data
+
+```java
+record OrderRequest(String product, int quantity) {}
+record OrderResponse(Long id, String status, BigDecimal total) {}
+```
+
+### Pattern Matching in Tests
+
+```java
+@Test
+void shouldHandleDifferentOrderTypes() {
+ var order = orderService.create(new OrderRequest("Product", 2));
+
+ switch (order) {
+ case PhysicalOrder po -> assertThat(po.getShippingAddress()).isNotNull();
+ case DigitalOrder do_ -> assertThat(do_.getDownloadLink()).isNotNull();
+ default -> throw new IllegalStateException("Unknown order type");
+ }
+}
+```
+
+### Text Blocks for JSON
+
+```java
+@Test
+void shouldParseComplexJson() {
+ var json = """
+ {
+ "id": 1,
+ "status": "PENDING",
+ "items": [
+ {"product": "Laptop", "price": 999.99},
+ {"product": "Mouse", "price": 29.99}
+ ]
+ }
+ """;
+
+ assertThat(mvc.post().uri("/orders")
+ .contentType(APPLICATION_JSON)
+ .content(json))
+ .hasStatus(CREATED);
+}
+```
+
+### Sequenced Collections
+
+```java
+@Test
+void shouldReturnOrdersInSequence() {
+ var orders = orderRepository.findAll();
+
+ assertThat(orders.getFirst().getStatus()).isEqualTo("NEW");
+ assertThat(orders.getLast().getStatus()).isEqualTo("COMPLETED");
+ assertThat(orders.reversed().getFirst().getStatus()).isEqualTo("COMPLETED");
+}
+```
+
+## Dependencies by Slice
+
+```xml
+
+
+ org.springframework.boot
+ spring-boot-starter-webmvc-test
+ test
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-restclient-test
+ test
+
+
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+```
diff --git a/skills/spring-boot-testing/references/testcontainers-jdbc.md b/skills/spring-boot-testing/references/testcontainers-jdbc.md
new file mode 100644
index 00000000..88015199
--- /dev/null
+++ b/skills/spring-boot-testing/references/testcontainers-jdbc.md
@@ -0,0 +1,234 @@
+# Testcontainers JDBC
+
+Testing JPA repositories with real databases using Testcontainers.
+
+## Overview
+
+Testcontainers provides real database instances in Docker containers for integration testing. More reliable than H2 for production parity.
+
+## PostgreSQL Setup
+
+### Dependencies
+
+```xml
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+
+ org.testcontainers
+ testcontainers-postgresql
+ test
+
+```
+
+### Basic Test
+
+```java
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+@Testcontainers
+class OrderRepositoryPostgresTest {
+
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:18");
+
+ @Autowired
+ private OrderRepository orderRepository;
+
+ @Autowired
+ private TestEntityManager entityManager;
+}
+```
+
+## MySQL Setup
+
+```xml
+
+ org.testcontainers
+ testcontainers-mysql
+ test
+
+```
+
+```java
+@Container
+@ServiceConnection
+static MySQLContainer> mysql = new MySQLContainer<>("mysql:8.4");
+```
+
+## Multiple Databases
+
+```java
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+@Testcontainers
+class MultiDatabaseTest {
+
+ @Container
+ @ServiceConnection(name = "primary")
+ static PostgreSQLContainer> primaryDb = new PostgreSQLContainer<>("postgres:18");
+
+ @Container
+ @ServiceConnection(name = "analytics")
+ static PostgreSQLContainer> analyticsDb = new PostgreSQLContainer<>("postgres:18");
+}
+```
+
+## Container Reuse (Speed Optimization)
+
+Add to `~/.testcontainers.properties`:
+
+```properties
+testcontainers.reuse.enable=true
+```
+
+Then enable reuse in code:
+
+```java
+@Container
+@ServiceConnection
+static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:18")
+ .withReuse(true);
+```
+
+## Database Initialization
+
+### With SQL Scripts
+
+```java
+@Container
+@ServiceConnection
+static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:18")
+ .withInitScript("schema.sql");
+```
+
+### With Flyway
+
+```java
+@SpringBootTest
+@Testcontainers
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+class MigrationTest {
+
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:18");
+
+ @Autowired
+ private Flyway flyway;
+
+ @Test
+ void shouldApplyMigrations() {
+ flyway.migrate();
+ // Test code
+ }
+}
+```
+
+## Advanced Configuration
+
+### Custom Database/Schema
+
+```java
+@Container
+@ServiceConnection
+static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:18")
+ .withDatabaseName("testdb")
+ .withUsername("testuser")
+ .withPassword("testpass")
+ .withInitScript("init-schema.sql");
+```
+
+### Wait Strategies
+
+```java
+@Container
+static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:18")
+ .waitingFor(Wait.forLogMessage(".*database system is ready.*", 1));
+```
+
+## Test Example
+
+```java
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+@Testcontainers
+class OrderRepositoryTest {
+
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:18");
+
+ @Autowired
+ private OrderRepository orderRepository;
+
+ @Autowired
+ private TestEntityManager entityManager;
+
+ @Test
+ void shouldFindOrdersByStatus() {
+ // Given
+ entityManager.persist(new Order("PENDING"));
+ entityManager.persist(new Order("COMPLETED"));
+ entityManager.flush();
+
+ // When
+ List pending = orderRepository.findByStatus("PENDING");
+
+ // Then
+ assertThat(pending).hasSize(1);
+ assertThat(pending.get(0).getStatus()).isEqualTo("PENDING");
+ }
+
+ @Test
+ void shouldSupportPostgresSpecificFeatures() {
+ // Can use Postgres-specific features like:
+ // - JSONB columns
+ // - Array types
+ // - Full-text search
+ }
+}
+```
+
+## @DynamicPropertySource Alternative
+
+If not using @ServiceConnection:
+
+```java
+@SpringBootTest
+@Testcontainers
+class OrderServiceTest {
+
+ @Container
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:18");
+
+ @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);
+ }
+}
+```
+
+## Supported Databases
+
+| Database | Container Class | Maven Artifact |
+| -------- | --------------- | -------------- |
+| PostgreSQL | PostgreSQLContainer | testcontainers-postgresql |
+| MySQL | MySQLContainer | testcontainers-mysql |
+| MariaDB | MariaDBContainer | testcontainers-mariadb |
+| SQL Server | MSSQLServerContainer | testcontainers-mssqlserver |
+| Oracle | OracleContainer | testcontainers-oracle-free |
+| MongoDB | MongoDBContainer | testcontainers-mongodb |
+
+## Best Practices
+
+1. Use @ServiceConnection when possible (Spring Boot 3.1+)
+2. Enable container reuse for faster local builds
+3. Use specific versions (postgres:18) not latest
+4. Keep container config in static field
+5. Use @DataJpaTest with AutoConfigureTestDatabase.Replace.NONE
diff --git a/skills/spring-boot-testing/references/webmvctest.md b/skills/spring-boot-testing/references/webmvctest.md
new file mode 100644
index 00000000..d96e8853
--- /dev/null
+++ b/skills/spring-boot-testing/references/webmvctest.md
@@ -0,0 +1,177 @@
+# @WebMvcTest
+
+Testing Spring MVC controllers with focused slice tests.
+
+## Basic Structure
+
+```java
+@WebMvcTest(OrderController.class)
+class OrderControllerTest {
+
+ @Autowired
+ private MockMvcTester mvc;
+
+ @MockitoBean
+ private OrderService orderService;
+
+ @MockitoBean
+ private UserService userService;
+}
+```
+
+## What Gets Loaded
+
+- The specified controller(s)
+- Spring MVC infrastructure (HandlerMapping, HandlerAdapter)
+- Jackson ObjectMapper (for JSON)
+- Exception handlers (@ControllerAdvice)
+- Spring Security filters (if on classpath)
+- Validation (if on classpath)
+
+## Testing GET Endpoints
+
+```java
+@Test
+void shouldReturnOrder() {
+ var order = new Order(1L, "PENDING", BigDecimal.valueOf(99.99));
+ given(orderService.findById(1L)).willReturn(order);
+
+ assertThat(mvc.get().uri("/orders/1"))
+ .hasStatusOk()
+ .hasContentType(MediaType.APPLICATION_JSON)
+ .bodyJson()
+ .extractingPath("$.status")
+ .isEqualTo("PENDING");
+}
+```
+
+## Testing POST with Request Body
+
+### Using Text Blocks (Java 25)
+
+```java
+@Test
+void shouldCreateOrder() {
+ given(orderService.create(any(OrderRequest.class))).willReturn(1L);
+
+ var json = """
+ {
+ "product": "Product A",
+ "quantity": 2
+ }
+ """;
+
+ assertThat(mvc.post().uri("/orders")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(json))
+ .hasStatus(HttpStatus.CREATED)
+ .hasHeader("Location", "/orders/1");
+}
+```
+
+### Using Records
+
+```java
+record OrderRequest(String product, int quantity) {}
+
+@Test
+void shouldCreateOrderWithRecord() {
+ var request = new OrderRequest("Product A", 2);
+ given(orderService.create(any())).willReturn(1L);
+
+ assertThat(mvc.post().uri("/orders")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(json.write(request).getJson()))
+ .hasStatus(HttpStatus.CREATED);
+}
+```
+
+## Testing Validation Errors
+
+```java
+@Test
+void shouldRejectInvalidOrder() {
+ var invalidJson = """
+ {
+ "product": "",
+ "quantity": -1
+ }
+ """;
+
+ assertThat(mvc.post().uri("/orders")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(invalidJson))
+ .hasStatus(HttpStatus.BAD_REQUEST)
+ .bodyJson()
+ .hasPath("$.errors");
+}
+```
+
+## Testing Query Parameters
+
+```java
+@Test
+void shouldFilterOrdersByStatus() {
+ assertThat(mvc.get().uri("/orders?status=PENDING"))
+ .hasStatusOk();
+
+ verify(orderService).findByStatus(OrderStatus.PENDING);
+}
+```
+
+## Testing Path Variables
+
+```java
+@Test
+void shouldCancelOrder() {
+ assertThat(mvc.put().uri("/orders/123/cancel"))
+ .hasStatusOk();
+
+ verify(orderService).cancel(123L);
+}
+```
+
+## Testing with Security
+
+```java
+@Test
+@WithMockUser(roles = "ADMIN")
+void adminShouldDeleteOrder() {
+ assertThat(mvc.delete().uri("/orders/1"))
+ .hasStatus(HttpStatus.NO_CONTENT);
+}
+
+@Test
+void anonymousUserShouldBeForbidden() {
+ assertThat(mvc.delete().uri("/orders/1"))
+ .hasStatus(HttpStatus.UNAUTHORIZED);
+}
+```
+
+## Multiple Controllers
+
+```java
+@WebMvcTest({OrderController.class, ProductController.class})
+class WebLayerTest {
+ // Tests multiple controllers in one slice
+}
+```
+
+## Excluding Auto-Configuration
+
+```java
+@WebMvcTest(OrderController.class)
+@AutoConfigureMockMvc(addFilters = false) // Skip security filters
+class OrderControllerWithoutSecurityTest {
+ // Tests without security filters
+}
+```
+
+## Key Points
+
+1. Always mock services with @MockitoBean
+2. Use MockMvcTester for AssertJ-style assertions
+3. Test HTTP semantics (status, headers, content-type)
+4. Verify service method calls when side effects matter
+5. Don't test business logic here - that's for unit tests
+6. Leverage Java 25 text blocks for JSON payloads