agentleFS
piominpiomin/claude-ai-spring-boot/skills/testing-patterns/SKILL.md

testing-patterns

Skill1.4k starsChanged 4 days agoApache-2.0View on GitHub

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.

44 other projects use a version of this, 1 of them edited.

Adopted by

Steady: 0 new projects took it in the last 4 weeks, none the 4 weeks before.thin data

Week of Sep 13: 0 new projectsWeek of Sep 20: 0 new projectsWeek of Sep 27: 0 new projectsWeek of Oct 4: 0 new projectsWeek of Oct 11: 0 new projectsWeek of Oct 18: 0 new projectsWeek of Oct 25: 0 new projectsWeek of Nov 1: 0 new projects
Sep 2026Oct 2026Nov 2026
New projects taking a version of this file each week, over the 8 weeks our latest run read: only 0 in all, so this is a hint, not a trend.

Most recently taken up by

Who's talking about piomin/claude-ai-spring-boot

The biggest 5 of 17 mentions linking to the repository that our crawl found. Each line is their own headline.

See all 17

Talked about on GitHub (17 times).

On GitHub · 17

12 more on GitHub

Used with

Projects with it also use intelligent-routing, bash-linux and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • SKB3002Skill

    intelligent-routing

    Automatic agent selection and intelligent task routing. Analyzes user requests and automatically selects the best specialist agent(s) without requiring explicit user mentions.

    Also in 5 projects that use this

  • xenitV1Skill

    bash-linux

    Bash/Linux terminal patterns. Critical commands, piping, error handling, scripting. Use when working on macOS or Linux systems.

    Also in 6 projects that use this

  • xenitV1Skill

    red-team-tactics

    Red team tactics principles based on MITRE ATT&CK. Attack phases, detection evasion, reporting.

    Also in 6 projects that use this

  • ChrisWilesSkill

    formik-patterns

    Formik form handling with validation patterns. Use when building forms, implementing validation, or handling form submission.

    Also in 3 projects that use this

  • SKB3002Skill

    nextjs-react-expert

    React and Next.js performance optimization from Vercel Engineering. Use when building React components, optimizing performance, eliminating waterfalls, reducing bundle size, reviewing code for performance issues, or implementing server/client-side optimizations.

    Also in 3 projects that use this

  • adybaSkill

    safe-workflow

    SAFe Agentic Workflow orchestrator. Teaches the full SAFe methodology adapted for AI agents: pattern discovery, exit states, quality gates, agent personas, and specs-driven development. Use when setting up a project with SAFe workflow, understanding the multi-agent methodology, or coordinating work across agent personas. Triggers: "SAFe workflow", "agent workflow", "safe harness", "specs-driven", "exit states", "quality gates", "multi-agent workflow".

    Also in 3 projects that use this

  • +4 more from xenitV1

The file

We split this skill into the rules it gives an agent

2 rules. 0 are common, 1 is distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 29 edited versions we read keep these.

  • 1

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Test failures or debugging test issues

    line 12
  2. Very specific:

    Run them with ./mvnw clean verify.

    line 345

“Common” is measured: kept by at least 6 in 10 of the 29 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

LevelAnnotationSpeedScopeWhen to Use
Unit@ExtendWith(MockitoExtension.class)FastSingle classBusiness logic in services
Slice@WebMvcTest, @DataJpaTestMediumOne layerControllers, repositories
Integration@SpringBootTestSlowFull appEnd-to-end flows, cross-layer

Unit Tests (Service Layer)

@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)

@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    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(content().contentType(MediaType.APPLICATION_PROBLEM_JSON))
            .andExpect(jsonPath("$.errors[0].field").value("name"))
            .andExpect(jsonPath("$.errors[0].code").value("required"));

        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 ResponseStatusException(HttpStatus.NOT_FOUND, "Product 999 not found"));

        mockMvc.perform(get("/api/v1/products/999"))
            .andExpect(status().isNotFound())
            .andExpect(jsonPath("$.status").value(404));
    }

    // ✅ Negative - stale version
    @Test
    @DisplayName("PUT /api/v1/products/1 - stale version returns 409 without errors")
    void updateProduct_staleVersion_returns409() throws Exception {
        when(productService.update(eq(1L), any()))
            .thenThrow(new ObjectOptimisticLockingFailureException(Product.class, 1L));

        mockMvc.perform(put("/api/v1/products/1")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"name":"Widget","price":10.0,"version":0}
                    """))
            .andExpect(status().isConflict())
            .andExpect(jsonPath("$.errors").doesNotExist());
    }
}
  • Assert error bodies as Problem Details ($.status, $.errors[*].field, $.errors[*].code), never on message texts.
  • @WebMvcTest creates no repositories or services: every bean the controller injects needs a @MockitoBean, including a repository newly added to an existing controller.

Repository Slice Tests (@DataJpaTest)

@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)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
@Testcontainers(disabledWithoutDocker = true)
@ActiveProfiles("test")
class ProductIntegrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @Autowired
    private RestTestClient restTestClient;

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private Flyway flyway;

    @Autowired
    private JdbcTemplate jdbc;

    @Autowired
    private BuildProperties buildProperties;

    @Test
    @DisplayName("Should apply all Flyway migrations")
    void migrationsApplied() {
        assertThat(flyway.info().current()).isNotNull();
        assertThat(flyway.info().pending()).isEmpty();
    }

    @Test
    @DisplayName("Should reject row violating DB constraint")
    void databaseRejectsNegativePrice() {
        // Migration defines CONSTRAINT ck_products_price CHECK (price >= 0)
        assertThatThrownBy(() -> jdbc.update(
                "INSERT INTO products (name, price, version) VALUES ('Widget', -1, 0)"))
            .isInstanceOf(DataIntegrityViolationException.class);
    }

    @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();
    }

    // Only when the API is built for a UI (public /actuator/info, see conventions)
    @Test
    @DisplayName("Health and info are public; other Actuator endpoints are not")
    void statusEndpointsArePublic() {
        restTestClient.get().uri("/actuator/health").exchange()
            .expectStatus().isOk()
            .expectBody().jsonPath("$.components.db.status").isEqualTo("UP");
        restTestClient.get().uri("/actuator/info").exchange()
            .expectStatus().isOk()
            .expectBody().jsonPath("$.build.version").isEqualTo(buildProperties.getVersion());
        restTestClient.get().uri("/actuator/env").exchange()
            .expectStatus().isUnauthorized();
    }
}

What the integration tests cover besides CRUD (the last two only when the API is built for a UI, see conventions):

  • Relations: none → A, A → B and A → none for every @ManyToOne the API lets a client change, against the real database. A → B is the case that fails with LazyInitializationException when the write is not @Transactional; a mocked @WebMvcTest cannot see it.
  • Referenced delete: deleting a record that is still referenced returns 409, and the row is still there.
  • Stale version: two updates with the same version; the second returns 409.
  • Contract export: the test that writes contract/openapi.json (spring-boot skill, references/ui-contract.md).
  • Status endpoints: health and info answer without a token, everything else under /actuator does not.

Run them with ./mvnw clean verify. With @Testcontainers(disabledWithoutDocker = true) they are skipped silently when Docker is not available, so check the test report shows them as run, not skipped. With Podman instead of Docker:

# macOS (podman machine); on Linux: unix:///run/user/$(id -u)/podman/podman.sock
export DOCKER_HOST=unix://$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}')
export TESTCONTAINERS_RYUK_DISABLED=true
./mvnw clean verify

Testcontainers Reusable Config

@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

@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

PatternUse For
@ExtendWith(MockitoExtension.class)Unit tests with mocks
@WebMvcTest(Controller.class)Controller tests with MockMvc
@DataJpaTestRepository tests with real DB
@SpringBootTest + RANDOM_PORTFull integration tests
@ServiceConnectionAuto-configure Testcontainers (3.1+)
@ParameterizedTestMultiple 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

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.