Skip to content
Skills Library
Programming · Testing

spring-testing

/spring-testing

Write, repair, and modernise tests in an existing Spring Boot 4 project (Maven or Gradle) — Testcontainers 2.x, integration vs unit routing, and the Boot 4 test API (@MockitoBean, MockMvcTester, RestTestClient). Use when asked to add or fix tests, set up Testcontainers, or migrate off Testcontainers 1.x or @MockBean.

Licence
MIT
Last repository change
11.11.2026

Assessment and security

100/ 100Class: good
Format25 / 25
Frontmatter with name and description, valid name, description up to 1,024 characters, name matching the folder.
Discoverability25 / 25
Description explains when to use the skill, has a reasonable length and uses third person.
Content35 / 35
At least 150 words and at most 500 lines, headings, step lists, examples or code.
Hygiene15 / 15
No placeholders such as TODO, no Windows paths, with a licence.
Riskno signals detected in the scan
Files in this package
3
Scripts
0
HTML comments
0
URLs in SKILL.md
0
Deprecation

No signals detected. Repository changed on 11.11.2026, without references to retired models or APIs.

Licence

MIT, open: use and modify while retaining author attribution.

What it does and when to use it

Write, repair, and modernise tests in an existing Spring Boot 4 project (Maven or Gradle) — Testcontainers 2.x, integration vs unit routing, and the Boot 4 test API (@MockitoBean, MockMvcTester, RestTestClient). Use when asked to add or fix tests, set up Testcontainers, or migrate off Testcontainers 1.x or @MockBean.

Original author description

Write, repair, and modernise tests in an existing Spring Boot 4 project (Maven or Gradle) — Testcontainers 2.x, integration vs unit routing, and the Boot 4 test API (@MockitoBean, MockMvcTester, RestTestClient). Use when asked to add or fix tests, set up Testcontainers, or migrate off Testcontainers 1.x or @MockBean.

How it works

  1. Step 0 — Read the project
  2. Step 1 — Audit before writing
  3. Step 2 — Route the work
  4. Step 3 — Ensure a base class exists
  5. Step 4 — Write the tests
  6. Step 5 — Run and report
Original SKILL.md headings · 239 lines, 1,175 words
SKILL.md preview
---
name: spring-testing
description: "Write, repair, and modernise tests in an existing Spring Boot 4 project (Maven or Gradle) — Testcontainers 2.x, integration vs unit routing, and the Boot 4 test API (@MockitoBean, MockMvcTester, RestTestClient). Use when asked to add or fix tests, set up Testcontainers, or migrate off Testcontainers 1.x or @MockBean."
---

# Spring Testing Skill

Owns the testing story for a project that already exists. `spring-scaffold` generates a
`BaseIntegrationTest` for new projects and stops there — this skill is what you run afterwards, on
every project, for the rest of its life.

`SKILL_DIR` = the directory containing this SKILL.md.

**Load before doing anything:**

- `SKILL_DIR/references/containers.md` — Testcontainers 2.x coordinates, class packages, pinned
  images, and the commands that verify them. The single source of truth for versions.
- `SKILL_DIR/references/conventions.md` — what gets a unit vs integration vs slice test, the Boot 4
  API replacements, mocking rules, isolation, flakiness.

Never add a test dependency without naming it and saying why. AssertJ, Mockito, JUnit 5 and
Awaitility already arrive with the Boot test starters — check before reaching for anything.

---

## Step 0 — Read the project

```bash
ls pom.xml build.gradle build.gradle.kts 2>/dev/null
grep -m1 -A1 'spring-boot-starter-parent' pom.xml
find src/test -name '*.java' | head -30
```

Establish: build tool, Boot version, whether a `BaseIntegrationTest` (or equivalent) already exists,
and what the existing tests actually do. Match what's there — do not restructure someone's suite as a
side effect of adding one test.

If the project is not on Boot 4, say so and stop before applying Boot 4 API guidance; the
replacements in `conventions.md` do not exist on 3.x.

---

## Step 1 — Audit before writing

Run all five. Each hit is a finding to report, and the first two are the difference between a suite
that is on Testcontainers 2.x and one that only looks like it.

```bash
# 1. Testcontainers 1.x coordinates (they resolve, they just stop at 1.21.4 forever)
grep -n -A1 'org.testcontainers' pom.xml \
  | grep -E '<artifactId>(junit-jupiter|postgresql|kafka|rabbitmq|mongodb|localstack|grafana|mysql|elasticsearch)</artifactId>' || true

# 2. deprecated 1.x container imports still compiling against the 2.x shims
grep -rn --include=*.java 'org.testcontainers.containers.[A-Z]' src/test/java \
  | grep -v GenericContainer || true

# 3. APIs removed in Boot 4
grep -rn --include=*.java -e '@MockBean' -e '@SpyBean' -e 'boot.test.web.client.TestRestTemplate' src/test/java || true

# 4. unpinned or floating images
grep -rn --include=*.java -e ':latest' -e 'DockerImageName.parse("[a-z/]*")' src/test/java || true

# 5. JUnit-managed containers in a shared base class (breaks once a second class reuses the context)
grep -rln --include=*.java '@Container' src/test/java | xargs -r grep -l 'abstract class' || true
```

Then confirm the resolved Testcontainers version against the BOM using the commands in
`references/containers.md` — never pin `testcontainers.version` by hand to fix a mismatch; that is a
Boot upgrade.

Report the findings as a short list before touching code. Fix them as part of the work only if the
user asked for tests in that area; otherwise flag and leave them.

---

## Step 2 — Route the work

Use the routing table in `references/conventions.md`. The short version:

- touches the database, a broker, or S3 → **integration test with a real container**
- pure logic, no IO → **unit test, no Spring context**
- HTTP contract only → **`@WebMvcTest` slice with `MockMvcTester`**
- outbound HTTP client → **`@RestClientTest` slice with `MockRestServiceServer`**

State which kind you're writing and why in one line, then write it.

---

## Step 3 — Ensure a base class exists

Every integration test extends one base class that imports one container configuration. If the
project has none, create both next to the application class in `src/test/java`:

```java
@TestConfiguration(proxyBeanMethods = false)
public class IntegrationTestContainers {

    @Bean
    @ServiceConnection
    PostgreSQLContainer postgresContainer() {
        return new PostgreSQLContainer(DockerImageName.parse("postgres:18-alpine"));
    }
}
```

```java
@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = {
        "management.otlp.metrics.export.enabled=false",
        "management.tracing.export.enabled=false",
        "management.logging.export.otlp.enabled=false"
    })
@Import(IntegrationTestContainers.class)
public abstract class BaseIntegrationTest {
}
```

**Containers are Spring beans, never `static @Container` fields in a shared base class.** The JUnit
`@Testcontainers` extension stops a static `@Container` after each test class, while Spring caches
the application context across classes — the next class reuses a context pointing at a stopped
container and fails with `Connection refused`. As a `@Bean`, the container lives exactly as long as
the cached context. A base class that already declares a `static @Container` (audit check 5) is a
latent failure: report it, and move the container into `IntegrationTestContainers` before adding a
second integration test class — that is the moment it breaks.

The `properties` disable OTLP export so tests don't warn against a collector that isn't running.
Telemetry is exercised in dev via `./mvnw spring-boot:test-run`, not in the test suite — the Grafana
LGTM container never belongs in `IntegrationTestContainers`.

For any other backing service, add a `@Bean @ServiceConnection` method to `IntegrationTestContainers`
with the class, artifact, and pinned tag from `references/containers.md`. One configuration per
project; every distinct set of imports is a separate cached context and a separate set of containers.

---

## Step 4 — Write the tests

**Repository / persistence:**

```java
class JobRepositoryIntegrationTest extends BaseIntegrationTest {

    @Autowired
    private JobRepository jobRepository;

    @Test
    @Sql("/sql/three-open-jobs.sql")
    void should_return_only_open_jobs_when_filtering_by_status() {
        var jobs = jobRepository.findByStatus(JobStatus.OPEN);

        assertThat(jobs).hasSize(3).allMatch(job -> job.getStatus() == JobStatus.OPEN);
    }
}
```

Field injection is banned in `src/main`; `@Autowired` on a test field is the accepted exception —
JUnit constructs the test class, so there is no constructor to inject through.

**HTTP contract, no database — `MockMvcTester`, not raw `MockMvc` matchers:**

```java
@WebMvcTest(JobController.class)
class JobControllerTest {

    @Autowired
    private MockMvcTester mvc;

    @MockitoBean
    private JobService jobService;

    @Test
    void should_return_400_with_problem_detail_when_title_is_blank() {
        assertThat(mvc.post().uri("/api/jobs").contentType(APPLICATION_JSON).content("""
                {"title": "", "description": "x"}
                """))
            .hasStatus(HttpStatus.BAD_REQUEST)
            .bodyJson().extractingPath("$.title").isEqualTo("Validation failed");
    }
}
```

**Full flow over real HTTP — `RestTestClient`, the `RestClient`-era replacement for
`TestRestTemplate`:**

```java
@AutoConfigureRestTestClient
class JobApiIntegrationTest extends BaseIntegrationTest {

    @Autowired
    private RestTestClient client;

    @Test
    void should_persist_and_return_created_job_when_request_is_valid() {
        client.post().uri("/api/jobs")
            .contentType(APPLICATION_JSON)
            .body(new JobRequest("Backend Engineer", "Spring Boot 4"))
            .exchange()
            .expectStatus().isCreated()
            .expectBody().jsonPath("$.title").isEqualTo("Backend Engineer");
    }
}
```

Rules that apply to all three: `should_<expected>_when_<condition>` names, AssertJ assertions, one
behaviour per test, no `@Transactional` on tests that go over HTTP, and error paths assert the
`ProblemDetail` fields — status, title, detail — not just that something threw.

---

## Step 5 — Run and report

```bash
./mvnw test -Dtest='<NewTest>'   # the new tests alone first
./mvnw test                       # then the whole suite — new tests must not break neighbours
```

First run pulls images; say so rather than letting it look hung. If Docker isn't running, stop and
say exactly that — `Docker daemon not reachable; Testcontainers cannot start` — never fall back to an
embedded database to make the run go green.

Lead with the verdict, then the board:

```
3 tests added · suite green · 1 setup finding

━━━ SPRING-TESTING ━━━━━━━━━━━━━━━━━━━━━━━━━
Setup audit ........... ⚠  1  pom.xml:71 org.testcontainers:junit-jupiter → testcontainers-junit-jupiter
Tests written ......... 3  (1 integration, 2 unit)
New tests ............. ✅ 3 passing
Full suite ............ ✅ 47 passing · 0 failed · 0 skipped
Runtime ............... 18s (was 11s — one new container class)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Next: fix the 1.x coordinate in pom.xml:71, then run a ship-check.
```

On failure show the failing assertion and its file:line, not the log — `… +180 more lines`, and say
the full log is one `./mvnw test` away. Never report a suite green without having run it.

Fix a failing test only when the test is what's wrong. A test that caught a real bug is the test
doing its job — say so, name the bug, and let the user decide.
Files in this package (3)
  • SKILL.md 9 KB
  • references/containers.md 10 KB
  • references/conventions.md 6 KB

Versions

The database has no other skills named spring-testing.

Database snapshot: 13.11.2026 · package version 5402c82c4b46 (SHA-256 digest of the entire package)

Downloads in the last 30 days: 0 · ranking

Report a problem with this skill: harmful content, incorrect descriptions or an author’s removal request.

Similar skills

  • 100spring-boot-testing
    Spring Boot 4 testing strategies and patterns. Use when writing unit tests, slice tests (@WebMvcTest, @DataJpaTest), integration tests, Testcontainers with @ServiceConnection, security testing (@WithMockUser, JWT), or Modulith event testing with Scenario API. Covers the critical…
  • 100spring-boot-testing
    How to test Spring Boot 4.x applications: unit tests with JUnit 5 and Mockito, slice tests (@WebMvcTest with MockMvcTester, @DataJpaTest), @SpringBootTest integration tests with Testcontainers and @ServiceConnection, test data setup, ArchUnit architecture rules, and which test…
  • 100322-frameworks-spring-boot-testing-integration-tests
    Use when you need to write or improve integration tests — including Testcontainers with @ServiceConnection, @DataJdbcTest persistence slices, TestRestTemplate or MockMvcTester for HTTP, data isolation, and container lifecycle management for Spring Boot 4.0.x. This should trigger…
  • 100spring-boot-integration
    Spring Boot integration testing with Testcontainers, sliced tests, and full context. Covers real database testing, API integration, and end-to-end flows. USE WHEN: user mentions "spring integration test", "testcontainers spring", "full context test", asks about "@SpringBootTest…
  • 100spring-boot-test-patterns
    Provides comprehensive testing patterns for Spring Boot applications covering unit, integration, slice, and container-based testing with JUnit 5, Mockito, Testcontainers, and performance optimization. Use when writing tests, @Test methods, @MockBean mocks, or implementing test…
  • 95backend-spring-test
    Use when reviewing Spring Boot test code — test slice selection (`@SpringBootTest` vs `@WebMvcTest` vs `@DataJpaTest`), `@MockBean` overuse, Testcontainers vs H2, `@DirtiesContext` hygiene, security context in tests. Triggers when Spring Boot test files are in scope.

Feedback and reports

Your message goes to Bartosz Sokoliński. An email address is needed only for a reply. See the privacy policy.