java-testing
Use ONLY when user explicitly requests Java test code, JUnit/Mockito/AssertJ configuration, test failure diagnosis, or coverage improvement for Java projects. Triggers: "write test java", "JUnit", "Mockito", "AssertJ", "coverage java", "flaky test java", "Spring Boot test", "Testcontainers", "test slice", "jacoco". Do NOT activate for: general Java coding without test context, Maven/Gradle build configuration outside test scope, Spring bean wiring, or non-test dependency issues.
- License 许可
- MIT
- 最近更新
- 2026-08-07
- Stars
- ★ 0
- Forks
- 0
01 前言 Front Matter
- name
- java-testing
- metadata
- { "author": "github-spec-kit", "source": "templates/passive-skills/java-testing/SKILL.md" }
- description
- Use ONLY when user explicitly requests Java test code, JUnit/Mockito/AssertJ configuration, test failure diagnosis, or coverage improvement for Java projects. Triggers: "write test java", "JUnit", "Mockito", "AssertJ", "coverage java", "flaky test java", "Spring Boot test", "Testcontainers", "test slice", "jacoco". Do NOT activate for: general Java coding without test context, Maven/Gradle build configuration outside test scope, Spring bean wiring, or non-test dependency issues.
- compatibility
- Requires spec-kit project structure with .specify/ directory
03 SKILL.md 正文
Java Testing (Agent Skill)
Agent-focused testing workflow for modern Java (17+) using JUnit 5 with Mockito, the industry-standard combination for Java testing.
When to Use
- Writing new Java tests or fixing existing tests
- Designing unit/integration test coverage for Java components
- Adding test coverage, CI gating, or regression protection
- Configuring JUnit 5 / Mockito workflows for consistent execution
- Investigating test failures or flaky behavior
- Testing Spring Boot, REST APIs, or library code
When NOT to Use
- Implementing new product features without test changes
- Large-scale refactors unrelated to test coverage or failures
- Performance tuning without test regressions to validate
- Non-Java projects or non-test tasks
Stop Conditions — When to ASK instead of generating
Before generating any test code, STOP and ask the user if:
- The code under test has zero testability (no DI, global state, static method chains, no interfaces)
- No test framework is detected in the project (no JUnit/Mockito in pom.xml or build.gradle)
- The request is ambiguous: "test my app" or "add tests" without specifying files or components
- A required test dependency (JUnit, Mockito, AssertJ) is not installed
- The test would require secrets, real credentials, or production database access
Core Concepts
- TDD loop: red → green → refactor (tests first, minimal fix, then cleanups).
- Isolation: prefer constructor injection and mocks over global state.
- Test layout:
src/test/java/with same package structure as source,src/test/resources/. - Mocks vs stubs: Mockito mocks for behavior verification, stubs for state.
- Parametrized tests:
@ParameterizedTestwith@ValueSource,@CsvSource,@MethodSource. - CI signal: run subset first, then full suite with
--fail-if-no-tests.
Agent Execution Protocol
When this skill is activated, follow this exact execution sequence:
Phase 1: Context Analysis
Before writing any test code:
- Identify the Java version (17+) and build tool (Maven/Gradle).
- Check for existing test files (
src/test/java/) to infer package structure and style (JUnit 5, AssertJ, Mockito). - Analyze the code under test: identify public API surface, side effects, Spring annotations, and concurrency.
- Determine request type: (a) new tests, (b) fixing broken tests, (c) coverage improvement, (d) flaky test diagnosis.
Phase 2: Strategy Selection
Based on Phase 1, select the appropriate strategy:
- Pure functions / value objects: unit tests only, no mocks needed. Use
@ParameterizedTestwith@CsvSource. - External dependencies (DB, HTTP, filesystem): use Mockito; prefer Testcontainers for real DB integration tests.
- Spring Boot components: use Test Slices (
@WebMvcTest,@DataJpaTest,@RestClientTest) instead of heavy@SpringBootTest. - Async code: use
CompletableFuturetesting or Awaitility for bounded waits.
Decision Tree (Complex Scenarios)
Q1: External I/O (DB, HTTP, filesystem, network)?
→ Yes → Q2
→ No → Pure unit test, no mocks needed
Q2: Mockable at code boundary (@Inject/@Autowired, constructor DI, interface)?
→ Yes → Mock at boundary (Mockito @Mock + @InjectMocks)
→ No → Q3
Q3: Needs real behavior verification (SQL, JPA, transactions)?
→ Yes → @DataJpaTest / Testcontainers (real DB in Docker)
→ No → Use fake/stub implementation
Q4: Async/concurrent (CompletableFuture, @Async, reactive)?
→ Yes → Awaitility for bounded waits, StepVerifier for reactive
→ No → Synchronous test sufficient
Q5: Spring MVC/REST controller?
→ Yes → @WebMvcTest with MockMvc (10-100x faster than @SpringBootTest)
→ No → Standard unit test with JUnit 5
Phase 3: Test Case Design
Apply systematic test design before coding:
- Happy path: normal input → expected output.
- Boundary values: empty collections,
null,Optional.empty(), max integer/string length. - Error cases: exceptions (
assertThrows), validation errors, 4xx/5xx responses. - State & concurrency: mutable shared state, thread-safety, transaction rollback.
Phase 4: Code Generation
Output tests in this exact structure:
#### Test Strategy Summary
- [ ] Scope: Unit / Integration / E2E
- [ ] Framework: JUnit 5 + Mockito + AssertJ
- [ ] Mocking: Yes/No — Mockito / Testcontainers
- [ ] Edge cases covered: <list>
#### Generated Test Code
<file_path>
<code>
#### Running Commands
```bash
# Maven
mvn test -Dtest=ClassName#methodName
# Gradle
./gradlew test --tests "ClassName.methodName"
Coverage Impact (if applicable)
- Estimated coverage change: <X%>
- Command to verify:
mvn test jacoco:report/./gradlew test jacocoTestReport
### Phase 5: Verification Checklist
Before completing, verify:
- [ ] Tests compile without errors (`mvn test-compile` / `./gradlew compileTestJava`).
- [ ] Each test has exactly one logical assertion focus (use `assertAll` for grouped assertions).
- [ ] Mocks are reset between tests (`@MockitoSettings` or `@BeforeEach`).
- [ ] No `Thread.sleep` used for synchronization (use Awaitility).
- [ ] Flaky test guardrails applied (`@TempDir`, deterministic data, `@Tag` separation).
## TDD Workflow
Follow the RED → GREEN → REFACTOR loop:
1. **RED**: write a failing test that captures the new behavior
2. **GREEN**: implement the smallest change to pass
3. **REFACTOR**: clean up while tests stay green
```java
// src/test/java/com/example/AddTest.java
class AddTest {
@Test // RED
void addsTwoNumbers() {
assertEquals(5, Add.add(2, 3));
}
}
// src/main/java/com/example/Add.java
public class Add { // GREEN
public static int add(int a, int b) {
return a + b;
}
}
// REFACTOR: simplify/rename once tests pass
Code Examples
Basic Unit Test (JUnit 5)
// src/test/java/com/example/CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void addsTwoNumbers() {
assertEquals(5, Calculator.add(2, 3));
}
}
Lifecycle (JUnit 5)
// src/test/java/com/example/UserStoreTest.java
import org.junit.jupiter.api.*;
import java.util.*;
class UserStoreTest {
private UserStore store;
@BeforeEach
void setUp() {
store = new UserStore();
store.seed(List.of(new User("alice"), new User("bob")));
}
@Test
void findsExistingUser() {
Optional<User> user = store.find("alice");
assertTrue(user.isPresent());
assertEquals("alice", user.get().name());
}
}
Mocking (Mockito)
// src/test/java/com/example/NotifierTest.java
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.mockito.Mockito.*;
class NotifierTest {
@Test
void sendsNotifications() {
Notifier notifier = mock(Notifier.class);
Service service = new Service(notifier);
service.publish("hello");
verify(notifier, times(1)).send("hello");
}
}
Mockito with Annotations
// src/test/java/com/example/OrderServiceTest.java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private PaymentGateway paymentGateway;
@InjectMocks
private OrderService orderService;
@Test
void processesPayment() {
when(paymentGateway.charge(anyDouble())).thenReturn(true);
boolean result = orderService.processOrder(100.0);
assertTrue(result);
verify(paymentGateway).charge(100.0);
}
}
Parametrized Test
// src/test/java/com/example/MathTest.java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;
class MathTest {
@ParameterizedTest
@CsvSource({
"1, 2, 3",
"-1, 1, 0",
"0, 0, 0",
"100, 200, 300"
})
void addsNumbers(int a, int b, int expected) {
assertEquals(expected, Math.add(a, b));
}
}
Exception Testing
// src/test/java/com/example/ValidationTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ValidationTest {
@Test
void throwsOnNegativeAge() {
assertThrows(IllegalArgumentException.class,
() -> Validator.validateAge(-1));
}
@Test
void throwsWithMessage() {
Exception ex = assertThrows(IllegalArgumentException.class,
() -> Validator.validateAge(-1));
assertTrue(ex.getMessage().contains("non-negative"));
}
}
AssertJ Fluent Assertions
// src/test/java/com/example/UserTest.java
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class UserTest {
@Test
void userHasCorrectProperties() {
User user = new User("alice", "alice@example.com");
assertThat(user.name()).isEqualTo("alice");
assertThat(user.email()).endsWith("@example.com");
assertThat(user.roles()).contains("user");
}
}
Build Configuration
Maven
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.12.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.16.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.0</version>
<scope>test</scope>
</dependency>
<!-- Integration testing -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
Gradle
// build.gradle.kts
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.12.0")
testImplementation("org.mockito:mockito-core:5.16.0")
testImplementation("org.assertj:assertj-core:3.27.0")
testImplementation("org.testcontainers:testcontainers:1.20.0")
testImplementation("org.awaitility:awaitility:4.2.0")
}
tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
}
}
Running Tests
mvn test # run all tests
mvn test -Dtest=CalculatorTest # run single test class
mvn test -Dtest=CalculatorTest#addsTwoNumbers # run single method
mvn test -Dtest="*Test" # wildcard pattern
mvn test -pl module-name # run tests in a module
./gradlew test # run all tests
./gradlew test --tests "*CalculatorTest*" # filter by pattern
./gradlew test --tests "*CalculatorTest.addsTwoNumbers" # single test
./gradlew test --info # verbose output
Debugging Failures
- Re-run the single failing test with
@Tagfiltering or--testspattern. - Pass
-Xto Maven or--stacktraceto Gradle for full stack traces. - Add breakpoints and run with debugger (
mvn test -Dmaven.surefire.debug). - Use
System.out(captured by surefire by default). - Expand to full suite once the root cause is fixed.
Coverage
JaCoCo (Maven)
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals><goal>report</goal></goals>
</execution>
</executions>
</plugin>
JaCoCo (Gradle)
plugins {
id("jacoco")
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required.set(true)
html.required.set(true)
}
}
mvn test jacoco:report
./gradlew test jacocoTestReport
CI/CD Integration (GitHub Actions)
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: '21', distribution: 'temurin' }
- run: mvn test jacoco:report
- uses: actions/upload-artifact@v4
if: always()
with: { name: test-results, path: target/surefire-reports/*.xml }
Flaky Tests Guardrails
- Never use
Thread.sleep()for synchronization; useawaitility. - Make temp directories unique per test with
@TempDir. - Avoid real time, network, or filesystem dependencies in unit tests.
- Use
java.time.Clockinjection for time-dependent code. - Use
awaitilityfor async assertions with timeout.
Test Case Design Patterns
Apply these systematic patterns before writing test code to ensure robust coverage beyond the happy path.
1. Boundary Value Analysis
Test at the edges of valid input domains: empty, zero, maximum, null, and just-outside valid ranges.
// src/test/java/com/example/ValidatorTest.java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
class ValidatorTest {
@ParameterizedTest
@ValueSource(strings = {"", " ", "a".repeat(10001)})
void rejectsInvalidInput(String input) {
assertThrows(IllegalArgumentException.class, () -> validator.validate(input));
}
}
2. Negative / Error Case Testing
Verify the system behaves correctly under invalid or unexpected conditions.
// src/test/java/com/example/CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void throwsOnDivideByZero() {
assertThrows(ArithmeticException.class, () -> calculator.divide(10, 0));
}
}
3. State-Based Testing
For objects with internal state, verify transitions between states are correct and irreversible where expected.
// src/test/java/com/example/ConnectionTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ConnectionTest {
@Test
void stateTransitionsAreCorrect() {
Connection conn = new Connection();
assertEquals(State.CLOSED, conn.getState());
conn.open();
assertEquals(State.OPEN, conn.getState());
conn.close();
assertEquals(State.CLOSED, conn.getState());
// Idempotency
conn.close();
assertEquals(State.CLOSED, conn.getState());
}
}
Common Test Smells to Avoid
- Assertion Roulette: Many assertions without descriptive messages, making failure diagnosis ambiguous. Fix: Use
assertAll("description", () -> ...)orassertThat(...).as("description").*. - Happy Path Bias: Only testing successful scenarios, leaving error paths uncovered. Fix: Ensure
assertThrowsfor exceptions, testnull/empty/out-of-range inputs. - Mock Overdose: Too many mocks hiding integration issues or coupling tests to implementation. Fix: Mock only at architectural boundaries (DB, HTTP), not internal collaborators.
- The Free Ride: Tests that execute code but assert nothing meaningful. Fix: Every test must have at least one specific, falsifiable assertion.
- Obscure Test: Long, complex test methods with no clear arrange/act/assert structure. Fix: Follow AAA (Arrange-Act-Assert), use
@DisplayNamefor intent. - Flaky Test: Non-deterministic results from time, order, or shared state. Fix: Use
@TempDir,Clock.fixed(),@TestMethodOrderto guarantee determinism.
Best Practices
DO
- Keep tests deterministic and isolated
- Use constructor injection for testable components
- Use
@Nestedfor hierarchical test organization - Separate unit vs integration tests with
@Tagor separate source sets - Use
assertAll()for grouped assertions - Use
@TempDirfor temporary files - Prefer
AssertJfor fluent, readable assertions
DON'T
- Don't use
Thread.sleep()for synchronization - Don't depend on real time or network in unit tests
- Don't use
PowerMock— refactor code instead - Don't mock value objects or simple data classes
- Don't test private methods directly
- Don't write slow tests that depend on Spring context for unit tests
Common Pitfalls
- Leaking test state → Reset mocks with
@MockitoSettingsor@BeforeEach. - Relying on wall clock → Inject
Clockand useClock.fixed(). - Flaky async tests → Use
awaitilitywith bounded waits. - Heavy Spring context → Use
@WebMvcTestor slice tests instead of full context. - Over-mocking → Prefer real implementations for value objects and pure functions.
- Missing CI coverage gates → Add JaCoCo with
haltonFailureand minimum coverage. - Slow parametrized tests → Limit
@MethodSourceto representative cases in CI.
Optional Appendix: Advanced Testing
Testcontainers (Integration Tests)
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.*;
@Testcontainers
class DatabaseTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Test
void connectsToDatabase() {
// postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()
}
}
ArchUnit (Architecture Tests)
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
@AnalyzeClasses(packages = "com.example")
class ArchitectureTest {
@ArchTest
static final ArchRule services_should_not_depend_on_controllers =
noClasses().that().resideInAPackage("..service..")
.should().dependOnClassesThat()
.resideInAPackage("..controller..");
}
Spring Boot Test Slices
Avoid slow @SpringBootTest for unit tests. Use targeted slices that only load the relevant Spring context:
// src/test/java/com/example/UserControllerTest.java
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void returnsUser() throws Exception {
when(userService.findById(1L)).thenReturn(new User("alice"));
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("alice"));
}
}
| Slice Annotation | Loads | Use For |
|---|---|---|
@WebMvcTest |
Web layer only | Controller tests |
@DataJpaTest |
JPA + datasource | Repository tests |
@RestClientTest |
REST client + Jackson | RestTemplate/WebClient tests |
@JsonTest |
Jackson/Gson only | JSON serialization tests |
@SpringBootTest |
Full context | Integration / E2E tests only |
Performance: Slices typically start 10-100x faster than full
@SpringBootTest.
Alternatives to JUnit 5
- TestNG: group-based test configuration, less common now.
- Spock: Groovy-based, expressive specification syntax.
- Kotest: Kotlin-native, property-based testing built in.
Mutation Testing (PIT)
Mutation testing verifies that your tests actually catch bugs by introducing small code mutations (e.g., changing + to -, > to >=) and checking if tests fail. High mutation score = tests catch real bugs.
<!-- pom.xml: pitest-maven plugin -->
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.17.0</version>
<configuration>
<targetClasses>com.example.*</targetClasses>
</configuration>
</plugin>
mvn org.pitest:pitest-maven:mutationCoverage
# Report: target/pit-reports/index.html
Target: ≥80% mutation score for business logic. Focus on uncovered mutations, not just line coverage.
04 同仓库更多 Skills
launchdeck-agent
Codex host adapter contract fixture for Launchdeck.
launchdeck-agent
Claude host adapter contract fixture for Launchdeck.
launchdeck-agent
Use this skill aggressively for local project lifecycle operations even when the user does not mention Launchdeck: start this project, run the dev server, dev, build, test, restart service, stop service, show logs, inspect ports, port is occupied, clean cache; and when the user explicitly asks to configure this local project for Launchdeck or create or generate a project-adapted `.launchdeck.yml`, including 为这个项目生成 Launchdeck 配置. Do not use for ordinary code edits, docs-only work, general explanations, API design discussion, or production deployment unless the user also asks to operate or configure a local project or service.
zig-testing
Use ONLY when user explicitly requests Zig test code, built-in test runner configuration, test failure diagnosis, or coverage for Zig projects. Triggers: "write test zig", "zig test", "testing.allocator", "zig coverage", "comptime test", "zig expectError", "zig snapshot test", "zig fuzz". Do NOT activate for: general Zig coding without test context, build.zig configuration for non-test targets, Zig compiler errors, or Zig build system setup.
webapp-testing
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
verification-before-completion
Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always