backend-testing · git:20260924.15dc64a · 2026-09-24 · sha256 79065cb72beba39f
backend-testing git:20260924.15dc64aA
Immutable. This exact content is served forever at /api/v1/blob/79065cb72beba39f.
---
name: backend-testing
description: Use before finishing any backend implementation in this Spring Boot repo — to choose the right evidence (unit, ownership, filter-chain, PostgreSQL, @TempDir, SSE), write focused JUnit 5 + Mockito + AssertJ tests, run the narrowest Gradle command, and report results honestly.
---
# Backend Testing — Playbook
> **Role:** HOW to prove a change. Binding evidence rules + setup facts: `skills/backend/TESTING.md` (read it too).
> **Requires:** `backend-core`.
## Step 1 — pick the evidence
Match the boundary you changed to a row of the evidence matrix in `TESTING.md`. Typical picks:
| You changed | Minimum evidence |
|---|---|
| Service method | Unit test: happy path + one failure path |
| Controller handler | Unit test on the controller with mocked service (status + body + error) |
| Owner-scoped read/write | Not-owned id → `404` and `verifyNoInteractions(downstream)` |
| Route access / filter / tokens | Filter-chain HTTP test (cookie + Bearer). If no harness exists, say so in the report |
| Migration / native SQL / MyBatis XML | Run on disposable PostgreSQL, or report "not validated on PostgreSQL" |
| Files | `@TempDir` test: traversal rejected, other owner rejected, cleanup on failure |
## Step 2 — write the test
Place it next to the existing ones: `src/test/java/com/bekaku/api/spring/{controller/api|serviceImpl}/{Subject}Test.java`.
```java
@ExtendWith(MockitoExtension.class)
class {Model}ControllerTest {
@Mock {Model}Service service;
@Mock I18n i18n;
private {Model}Controller controller;
@BeforeEach
void setUp() throws Exception {
controller = new {Model}Controller(service);
Field f = BaseResponseException.class.getDeclaredField("i18n"); // error helpers need it
f.setAccessible(true);
f.set(controller, i18n);
}
@Test
void findOneReturns404WhenMissing() {
when(i18n.getMessage("error.dataNotfound")).thenReturn("Data not found");
when(service.findById(1L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> controller.findOne(1L))
.isInstanceOfSatisfying(ApiException.class,
e -> assertThat(e.getApiError().getStatus()).isEqualTo(HttpStatus.NOT_FOUND));
}
}
```
- Name tests by behavior (`rejectsChatNotOwnedByUser`, `createReturns201`).
- Cover the denied/failure path, not only the happy path.
- Standalone MockMvc (`MockMvcBuilders.standaloneSetup(controller)`) is fine for request/response shape; it is not security evidence.
## Step 3 — run, narrowest first
```bash
./gradlew test --tests '*{Model}ControllerTest'
./gradlew compileJava
```
Run the full `./gradlew test` only for shared base classes, security chain, migrations, or build config changes. Run `./gradlew bootJar` only when packaging is affected.
## Step 4 — report
```text
Commands run: <exact command> → <PASS/FAIL + counts>
Evidence type: unit | ownership | filter-chain | PostgreSQL | @TempDir | SSE
Not verified: <e.g. real filter chain, PostgreSQL migration, external service>
```
For documentation-only changes: verify paths, links, and claims against the code; state that no runtime validation applies.
## Common mistakes
| Mistake | Fix |
|---|---|
| `NullPointerException` from `responseErrorNotfound()` in a test | Inject `I18n` into `BaseResponseException` via reflection |
| `401` in a filter-chain test or curl with a valid token | Send an `Accept-Apiclient` header too |
| Claiming route security from a controller unit test | Say "filter chain not verified" or add a real chain test |
| "App started, migration OK" | Dev uses `ddl-auto: update`; Flyway is off |
| Using H2 for PostgreSQL SQL | Disposable PostgreSQL + pgvector |
| Running the whole suite by default | Focused test + `compileJava` first |
| Adding `@WebMvcTest` / Testcontainers dependencies silently | Dependency change — ask first |