Java testing
Use when writing or reviewing Java tests, unit or integration — JUnit Jupiter with AssertJ assertions, the @DisplayName / method-name mirroring convention, behavior-focused naming, Mockito (annotation mocks, no deep stubs, @MockitoBean for Spring), and Testcontainers with @ServiceConnection for real backing services (database, Kafka, Redis). Catches JUnit-assertion use, class-level @DisplayName, imperative or deep-stub mocking, H2-instead-of-real-DB, container-per-method, and slow/flaky suites.From its SKILL.md
npx -y skills add mtkhawaja/java-skills --skill java-testingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
8.5 KB, ~1.7k tokens by cl100k_base, as published. Nobody here has run it
Java Testing
Overview
Tests document behavior. The house stack: JUnit Jupiter for structure, AssertJ for
assertions, Mockito (annotation-driven) for isolation, and Testcontainers +
@ServiceConnection when a test needs a real backing service. For container lifecycle and
suite-wide sharing details see references/testcontainers-patterns.md.
Structure & assertions
- JUnit Jupiter (5+) by default, unless the repo standardizes on something else.
- AssertJ for assertions —
assertThat(x).isEqualTo(y)/assertThatThrownBy(...), never JUnit'sassertEquals/assertTrue. - Every test has a method-level
@DisplayNamein natural language. Never at the class level (including@Nested). - The method name mirrors the
@DisplayNamein camelCase and describes behavior, not implementation:shouldDoXWhenY,shouldNotDoXWhenYIsCondition. Never atestprefix. - One behavior per test; order-independent and parallel-safe (no shared mutable state).
- Prefer
@ParameterizedTestfor input matrices and edge-case grids over duplicated bodies. - Never skip/disable/comment out a test to make the build pass.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class NotificationServiceTest {
@DisplayName("Should save published event When the producer publishes a new event")
@Test
void shouldSavePublishedEventWhenTheProducerPublishesANewEvent() {
assertThat(service.publish(event).status()).isEqualTo(Status.SAVED);
}
}
Mockito
- Use the JUnit Jupiter integration:
@ExtendWith(MockitoExtension.class). Don't callMockitoAnnotations.openMocks(...)or create mocks imperatively withmock(...). - Prefer annotation mocks:
@Mock,@Spy,@Captor,@InjectMocks. - Avoid brittle setups — no deep stubbing or mock-heavy tests tied to implementation details; prefer fakes or real collaborators. Stub only inside the test that needs it (no global stubbing).
- For Spring-managed beans use
@MockitoBean(prefer over the older@MockBean).
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock OrderRepository repository;
@InjectMocks OrderService service;
}
@SpringBootTest
class PaymentControllerTest {
@MockitoBean PaymentService paymentService; // prefer over @MockBean
}
Integration tests (Testcontainers)
Integration tests exercise the real backing service against production parity, fast and isolated:
- Use a real engine via Testcontainers, not H2/in-memory. H2's compatibility mode hides
dialect/type/identity/
RETURNINGdifferences — doubly true for Spring Data JDBC, which emits SQL almost literally. Match the prod engine and major version (postgres:16-alpine). - Wire it with
@ServiceConnection(Spring Boot 3.1+, needs thespring-boot-testcontainerstest dependency). It auto-creates the*ConnectionDetailsbean — prefer it over manual@DynamicPropertySource, which is the older, more verbose fallback. - Container fields are
static. A non-static@Containerstarts a fresh container per test method — slow. Static = once per class. - Share containers across the whole suite, not per class — a shared singleton (or a Spring-managed
container in an imported
@TestConfigurationbase) so the suite starts one Postgres, not one per test class. See the reference. - Isolate state between tests. Don't let one test's data leak into another. Use
@Transactionalrollback (repository slice tests) or reset (truncate / Flyway clean) in setup; never rely on order. - Use the narrowest slice that exercises the behavior:
@DataJdbcTest(JDBC beans only, rolls back per test) for repository tests;@SpringBootTestonly when you need the full service path. - Separate integration tests from unit tests so a slow container suite doesn't run on every
mvn test: name them*IT(Maven Failsafe) vs*Test(Surefire), and/or@Tag("integration"). - Don't hand-manage
start()/stop()for@Containerfields (the extension owns the lifecycle) — only for the deliberate singleton/reuse patterns in the reference.
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
// ... plus the usual JUnit/AssertJ/Spring imports
@Testcontainers
@SpringBootTest
class OrderPersistenceIT {
@Container
@ServiceConnection // auto-wires spring.datasource.* — no @DynamicPropertySource
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired OrderService orderService;
@Autowired OrderRepository orderRepository;
@DisplayName("Should persist order and read it back When place is called")
@Test
void shouldPersistOrderAndReadItBackWhenPlaceIsCalled() {
var placed = orderService.place(new Order(null, "customer-42", BigDecimal.valueOf(99.95)));
assertThat(orderRepository.findById(placed.id())).get()
.satisfies(found -> assertThat(found.customerId()).isEqualTo("customer-42"));
}
}
Common mistakes
| Rationalization | Reality |
|---|---|
"assertEquals is fine / imported" | House standard is AssertJ — reads as behavior, better failures. |
"@DisplayName on the @Nested class groups nicely" | Class-level @DisplayName is forbidden; put it on each method. |
"testGet is clear enough" | Behavior names: shouldReturnAccountWhenIdExists. No test prefix. |
"openMocks in @BeforeEach" | Use @ExtendWith(MockitoExtension.class) + @Mock. |
| "Deep stubs keep the test short" | Brittle — restructure or use a fake/real collaborator. |
"I'll @Disabled this flaky one" | Never disable tests to go green; fix the test or the code. |
| "H2 in-memory is faster for tests" | It hides real SQL/dialect bugs — Spring Data JDBC especially. Use the prod engine. |
"@DynamicPropertySource works fine" | @ServiceConnection is the modern, terse replacement (Boot 3.1+). |
| "Each test/class gets its own container" | Non-static @Container = container per method; per class is still slow at scale. static, shared across the suite. |
| "Integration tests run with the unit tests" | Separate *IT/Failsafe from *Test/Surefire so mvn test stays fast. |
Red flags — stop
org.junit.jupiter.api.Assertions/assertEqualsimported@DisplayNameon a class or@Nestedclass; atest-prefixed name; name ≠ its@DisplayNameMockitoAnnotations.openMocks, imperativemock(...), deep stubs,@MockBeanwhen@MockitoBeanexists- Tests sharing static/mutable state, or depending on execution order / leftover data
H2/hsqldb/:mem:standing in for the production database- A non-
static@Container; manual@DynamicPropertySourcewhere@ServiceConnectionapplies - A
@SpringBootTestwhere a@DataJdbcTest/@WebMvcTestslice would do
What ships with it: 1 file
4.4 KB alongside SKILL.md