Generates unit tests for Java code following TDD practices with 100% delta coverage. Use when the user asks for tests, mentions coverage, works with test files, or requests test generation for staged changes.
Activate when the user wants to:
given_when_then namingCopy this checklist and track progress:
Unit Test Progress:
- [ ] Phase 0: Check Input
- [ ] Phase 1: Scope Analysis
- [ ] Phase 2: Pre-Flight Check
- [ ] Phase 3: Test Generation
- [ ] Phase 4: Validation Loop
- [ ] Phase 5: Self-Review
- [ ] Phase 6: Output
git status
git diff --staged --name-only
Decision tree:
"No staged/unstaged changes found. Please specify the class or method to test."git diff --staged
For each modified file:
Ignore these patterns:
target/, build/, .gradle/*Test.java, *IT.java (existing tests)*.generated.java, *_.java (MapStruct)# Maven
mvn test -pl <module> -Dtest=*Test
# Gradle
./gradlew test --tests "*Test"
Decision tree:
For every modified class, apply the appropriate strategy.
Strategy: Verify @Builder, getters, setters, equals/hashCode.
@Test
void givenBuilder_whenBuildEntity_thenAllFieldsSet() {
MyEntity entity = MyEntity.builder()
.id(1L)
.name("test")
.build();
Assertions.assertThat(entity.getId()).isEqualTo(1L);
Assertions.assertThat(entity.getName()).isEqualTo("test");
}
Setup:
@ExtendWith(MockitoExtension.class)
class MyServiceTest {
@Mock
private MyRepository repository;
@InjectMocks
private MyServiceImpl service;
}
Test cases required:
| Case | Description | |------|-------------| | Happy Path | Valid input, expected output | | Exception Handling | Verify exception types and messages | | Null Inputs | If not validated by annotations | | Edge Cases | Boundary values, empty collections |
See references/CHECKLIST.md for detailed patterns.
| Rule | Implementation |
|------|----------------|
| Strict stubs | @ExtendWith(MockitoExtension.class) |
| Stubbing | Mockito.doReturn().when() (avoids real method call) |
| Stub argument matching | Mockito.any(Type.class) in stubs, never a concrete mutable instance |
| Verify single call | Mockito.verify(mock) — defaults to exactly one invocation; do not append Mockito.times(1) |
| Verify N calls | Mockito.verify(mock, Mockito.times(N)) only when N != 1 |
| Verify negative flow | Mockito.verify(mock, Mockito.never()) or Mockito.verifyNoInteractions(mock) |
| No static imports | Use Mockito.doReturn(), Assertions.assertThat() |
[!CAUTION] Never reuse mutable objects between Action and Assertion phases.
// BAD - Reference mutation bug
User user = new User();
user.setName("test");
service.save(user);
Mockito.verify(repository).save(user); // WRONG
// GOOD - ArgumentCaptor
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
Mockito.verify(repository).save(captor.capture());
Assertions.assertThat(captor.getValue().getName()).isEqualTo("test");
// GOOD - ArgumentMatchers
Mockito.verify(repository).save(Mockito.argThat(u ->
"test".equals(u.getName())
));
[!CAUTION] Never let a stubbed return value and its assertion expectation share the same heap reference.
// BAD - mockResponse IS expectedResponse (same reference)
ResponseDto expectedResponse = ResponseDto.builder().id(VALID_ID).status(STATUS_ACTIVE).build();
Mockito.doReturn(expectedResponse).when(mapper).toDto(Mockito.any());
ResponseDto result = service.process(VALID_ID);
Assertions.assertThat(result).usingRecursiveComparison().isEqualTo(expectedResponse);
// If 'service' mutates the object in place, both variables mutate together -> false positive (mutation survivor)
// GOOD - independent instances
ResponseDto mockResponse = ResponseDto.builder().id(VALID_ID).status(STATUS_ACTIVE).build();
ResponseDto expectedResponse = ResponseDto.builder().id(VALID_ID).status(STATUS_ACTIVE).build();
Mockito.doReturn(mockResponse).when(mapper).toDto(Mockito.any());
ResponseDto result = service.process(VALID_ID);
Assertions.assertThat(result).usingRecursiveComparison().isEqualTo(expectedResponse);
The same rule applies to stub arguments: pass Mockito.any(Type.class) in when()/doReturn().when() instead of a concrete request object, then verify the actual argument separately with ArgumentCaptor. Matching on a concrete reference couples stubbing to input validation and causes silent null returns (masked as downstream NullPointerException) if the SUT mutates the object before the call.
Prefer usingRecursiveComparison() over asserting one or two scalar fields, especially for DTOs with 3+ fields. Partial assertions let dropped or unmapped fields survive mutation testing undetected.
// BAD - only 1 of N fields verified
Assertions.assertThat(captor.getValue().getEmail()).isEqualTo(VALID_EMAIL);
// GOOD - full field graph comparison
Assertions.assertThat(captor.getValue()).usingRecursiveComparison().isEqualTo(expectedRequest);
Do not define mutable fixtures as class fields or in @BeforeEach; shared mutable state leaks side effects across tests and creates order-dependent failures. Generate fresh instances per test via a static factory (e.g., TestObjectFactory), and centralize magic literals there as public static final constants.
MAX_RETRIES = 3
Repeat this cycle:
# Maven
mvn clean verify -pl <module>
# Gradle
./gradlew test jacocoTestReport
# Generate diff
git diff origin/master > target/site/jacoco/diff.patch
# Run diff-cover
diff-cover target/site/jacoco/jacoco.xml \
--compare-branch origin/master \
--diff-file target/site/jacoco/diff.patch
Decision tree:
| Condition | Action |
|-----------|--------|
| Coverage == 100% | Proceed to Phase 5 |
| Coverage < 100% | Read missing lines, generate targeted tests |
| diff-cover unavailable | Use JaCoCo HTML report manually |
| Retries > 3 | HALT. Report: "Unable to achieve 100% delta coverage. Manual intervention required for: [uncovered lines]" |
For each missing line:
Apply code review protocols to generated tests.
| Check | Fix |
|-------|-----|
| Magic strings/numbers | Extract to private static final constants (or TestObjectFactory) |
| @Autowired usage | Replace with @InjectMocks |
| Static imports | Remove, use class names |
| StringUtils.EMPTY | Use for empty string literals |
| Mutable object reuse | Apply ArgumentCaptor pattern |
| Mock return == assertion expectation | Build two distinct instances (see Phase 3.E) |
| Concrete instance in stub matcher | Replace with Mockito.any(Type.class) |
| Partial field assertion | Replace with usingRecursiveComparison() |
| Mockito.verify(mock, Mockito.times(1)) | Simplify to Mockito.verify(mock) |
| Shared mutable @BeforeEach fixture | Replace with static factory method per test |
Follow Rules for Agent (Strict Architect Persona):
given_when_then format✅ Test Generation Complete
Coverage: 100% on new/modified lines
Tests Generated: [count]
Files:
- [TestClass1.java]
- [TestClass2.java]
diff-cover Summary:
Total: [X]% coverage on changed lines
Missing: [list any uncovered lines or "none"]
⚠️ Test Generation Incomplete
Coverage: [X]%
Uncovered Lines:
- [File.java:L45] - [reason]
- [File.java:L78] - [reason]
Manual intervention required.
| Action | Maven | Gradle |
|--------|-------|--------|
| Run tests | mvn test | ./gradlew test |
| Coverage report | mvn verify | ./gradlew jacocoTestReport |
| Specific test | mvn test -Dtest=MyTest | ./gradlew test --tests MyTest |
| Pattern | Example |
|---------|---------|
| Happy path | givenValidUser_whenSave_thenReturnsId |
| Exception | givenNullInput_whenSave_thenThrowsException |
| Edge case | givenEmptyList_whenProcess_thenReturnsEmpty |
// Equality
Assertions.assertThat(actual).isEqualTo(expected);
// Null checks
Assertions.assertThat(result).isNotNull();
// Collections
Assertions.assertThat(list).hasSize(3).contains(item);
// Exceptions
Assertions.assertThatThrownBy(() -> service.method(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("must not be null");
Rules for Agent (Strict Architect Persona).Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer