Junit expert
The official Ortus AI Skills repo
npx -y skills add Ortus-Solutions/skills --skill junit-expertAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 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.
What its author says it does
Copied from the file, not written here
Use when writing, reviewing, or structuring JUnit 5 tests for Java applications. Invoke for test lifecycle, parameterized tests, test extensions, assertions, test organization, parallel execution, test suites, dynamic tests, and integration with build tools.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
16.0 KB, as published. Nobody here has run it
JUnit Expert
JUnit 5 (Jupiter) specialist for comprehensive, maintainable, and expressive Java test suites.
Role Definition
Designs and implements JUnit 5 test suites that are fast, isolated, readable, and aligned with the test pyramid. Applies the full JUnit 5 API surface pragmatically — from basic lifecycle management to advanced extension authoring and dynamic test generation.
When to Use This Skill
- Writing new unit or integration tests for Java classes
- Restructuring disorganized or fragile test classes
- Implementing parameterized or data-driven test scenarios
- Authoring custom JUnit 5 extensions for cross-cutting concerns
- Configuring parallel test execution and test suite composition
- Diagnosing flaky or slow test behavior
Core Workflow
- Identify the unit under test and its collaborators
- Define test scope: unit, slice, or integration
- Structure the test class with appropriate lifecycle hooks
- Write focused, single-assertion-per-test cases
- Cover edge cases with parameterized inputs
- Validate build integration and execution time
JUnit 5 Architecture
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
| Component | Role |
|---|---|
| Platform | Test engine launcher, IDE/build integration |
| Jupiter | New programming and extension model (use this) |
| Vintage | Backward compatibility runner for JUnit 3/4 |
Dependency Setup
Maven
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<!-- Maven Surefire plugin must be 2.22.0+ -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
Gradle
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.withType<Test> {
useJUnitPlatform()
}
Test Lifecycle Annotations
| Annotation | Scope | Runs |
|---|---|---|
@Test | method | marks a test method |
@BeforeAll | static method | once before all tests in the class |
@AfterAll | static method | once after all tests in the class |
@BeforeEach | method | before each test method |
@AfterEach | method | after each test method |
@Disabled | class or method | skips the test |
@Tag | class or method | categorizes for filtering |
@Timeout | method | fails if execution exceeds duration |
Lifecycle Example
@Tag("unit")
class OrderServiceTest {
private OrderService service;
@BeforeEach
void setUp() {
service = new OrderService(new InMemoryOrderRepository());
}
@AfterEach
void tearDown() {
// release resources if needed
}
@Test
void shouldAcceptValidOrder() {
var order = Order.of("item-1", 3);
var result = service.place(order);
assertThat(result.status()).isEqualTo(OrderStatus.ACCEPTED);
}
@Test
@Disabled("pending inventory service integration")
void shouldRejectOutOfStockOrder() { }
}
Assertions (JUnit Jupiter + AssertJ)
Prefer AssertJ for rich, fluent assertions. Use JUnit built-ins for basic cases.
JUnit Built-in Assertions
import static org.junit.jupiter.api.Assertions.*;
// equality
assertEquals(expected, actual);
assertEquals(expected, actual, "message on failure");
// nullability
assertNotNull(result);
assertNull(result);
// boolean
assertTrue(condition);
assertFalse(condition);
// exceptions
assertThrows(IllegalArgumentException.class, () -> service.place(null));
var ex = assertThrows(OrderException.class, () -> service.cancel(closedOrder));
assertEquals("Order already closed", ex.getMessage());
// grouped assertions — all run even if one fails
assertAll(
() -> assertEquals("Alice", user.name()),
() -> assertEquals("[email protected]", user.email()),
() -> assertTrue(user.isActive())
);
// timeout
assertTimeout(Duration.ofMillis(200), () -> service.process(largeOrder));
AssertJ (Preferred)
import static org.assertj.core.api.Assertions.*;
// object
assertThat(result).isNotNull().isInstanceOf(Order.class);
// strings
assertThat(result.id()).startsWith("ORD-").hasSize(10);
// collections
assertThat(orders).hasSize(3).extracting(Order::status)
.containsExactly(ACCEPTED, ACCEPTED, REJECTED);
// exceptions
assertThatThrownBy(() -> service.place(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("order must not be null");
// optionals
assertThat(service.findById("missing")).isEmpty();
Nested Tests (@Nested)
Use @Nested to express hierarchical test structure for complex units.
class PaymentProcessorTest {
@Nested
@DisplayName("when payment method is credit card")
class CreditCardPayment {
@Test
void shouldAuthorizeValidCard() { ... }
@Test
void shouldDeclineExpiredCard() { ... }
@Nested
@DisplayName("and 3DS is required")
class With3DS {
@Test
void shouldTrigger3DSChallenge() { ... }
}
}
@Nested
@DisplayName("when payment method is bank transfer")
class BankTransfer {
@Test
void shouldQueueTransfer() { ... }
}
}
Parameterized Tests
@ValueSource — single primitive/string argument
@ParameterizedTest
@ValueSource(strings = {"", " ", "\t", "\n"})
void shouldRejectBlankNames(String name) {
assertThatThrownBy(() -> new User(name, "[email protected]"))
.isInstanceOf(IllegalArgumentException.class);
}
@ParameterizedTest
@ValueSource(ints = {-1, 0, Integer.MIN_VALUE})
void shouldRejectNonPositiveQuantity(int qty) {
assertThatThrownBy(() -> Order.of("item", qty))
.isInstanceOf(IllegalArgumentException.class);
}
@CsvSource — multiple inline arguments
@ParameterizedTest
@CsvSource({
"STANDARD, 3, 9.99",
"EXPRESS, 1, 19.99",
"OVERNIGHT, 0, 34.99"
})
void shouldCalculateShippingCost(String tier, int days, double expectedCost) {
var cost = shippingCalculator.calculate(ShippingTier.valueOf(tier), days);
assertThat(cost).isEqualByComparingTo(BigDecimal.valueOf(expectedCost));
}
@CsvFileSource — CSV from classpath file
@ParameterizedTest
@CsvFileSource(resources = "/test-data/orders.csv", numLinesToSkip = 1)
void shouldProcessOrderFromFile(String orderId, int quantity, String expected) { ... }
@MethodSource — complex object arguments
@ParameterizedTest
@MethodSource("invalidOrders")
void shouldRejectInvalidOrders(Order order, String expectedMessage) {
assertThatThrownBy(() -> service.place(order))
.hasMessageContaining(expectedMessage);
}
static Stream<Arguments> invalidOrders() {
return Stream.of(
Arguments.of(Order.of(null, 1), "item must not be null"),
Arguments.of(Order.of("", 1), "item must not be blank"),
Arguments.of(Order.of("item", 0), "quantity must be positive")
);
}
@EnumSource — enum values
@ParameterizedTest
@EnumSource(value = OrderStatus.class, names = {"CANCELLED", "REJECTED"})
void shouldNotAllowModificationOfTerminalOrders(OrderStatus status) {
var order = Order.withStatus(status);
assertThatThrownBy(() -> service.modify(order))
.isInstanceOf(IllegalStateException.class);
}
Dynamic Tests (@TestFactory)
Generate tests at runtime when the set of cases is not known statically.
@TestFactory
Stream<DynamicTest> shouldValidateAllCountryCodes() {
return CountryRegistry.allCodes().stream()
.map(code -> dynamicTest(
"should accept country code: " + code,
() -> assertThat(validator.isValid(code)).isTrue()
));
}
@TestFactory
Collection<DynamicContainer> shouldValidateProductRules() {
return List.of(
dynamicContainer("physical products", Stream.of(
dynamicTest("requires weight", () -> ...),
dynamicTest("requires dimensions", () -> ...)
)),
dynamicContainer("digital products", Stream.of(
dynamicTest("requires download URL", () -> ...)
))
);
}
JUnit 5 Extensions
Extensions replace JUnit 4 @Rule / @ClassRule. Implement one or more extension interfaces.
Common Extension Interfaces
| Interface | Purpose |
|---|---|
BeforeEachCallback | before each test |
AfterEachCallback | after each test |
BeforeAllCallback | before all tests in class |
AfterAllCallback | after all tests in class |
ParameterResolver | inject parameters into test methods |
TestExecutionExceptionHandler | handle/transform exceptions |
TestInstancePostProcessor | post-process test instance after creation |
ConditionEvaluationResult | control whether test runs |
Custom Extension Example — Database Cleanup
public class DatabaseCleanupExtension implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
getDatabase(context).beginTransaction();
}
@Override
public void afterEach(ExtensionContext context) {
getDatabase(context).rollback();
}
private Database getDatabase(ExtensionContext ctx) {
return ctx.getStore(Namespace.create(getClass(), ctx.getRequiredTestClass()))
.getOrComputeIfAbsent("db", k -> Database.connect(), Database.class);
}
}
@ExtendWith(DatabaseCleanupExtension.class)
class OrderRepositoryTest { ... }
@RegisterExtension (programmatic)
class UserServiceTest {
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
@Test
void shouldCallExternalApi() {
wireMock.stubFor(get("/users/1").willReturn(okJson("{\"name\":\"Alice\"}")));
var user = service.fetchUser(1L);
assertThat(user.name()).isEqualTo("Alice");
}
}
Conditional Test Execution
@Test
@EnabledOnOs(OS.LINUX)
void shouldUseLinuxFilePermissions() { ... }
@Test
@EnabledOnJre(JRE.JAVA_21)
void shouldUseVirtualThreads() { ... }
@Test
@EnabledIfSystemProperty(named = "env", matches = "ci")
void shouldRunOnlyInCI() { ... }
@Test
@EnabledIfEnvironmentVariable(named = "RUN_SLOW_TESTS", matches = "true")
void shouldRunSlowIntegrationTest() { ... }
Parallel Test Execution
Enable in src/test/resources/junit-platform.properties:
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=2
Control per-class or per-method:
@Execution(ExecutionMode.CONCURRENT)
class FastIsolatedTest { ... }
@Execution(ExecutionMode.SAME_THREAD)
class SharedStateTest { ... }
Warning: Do not enable concurrent mode for tests sharing mutable static state or external resources without synchronization.
Test Suites
@Suite
@SelectPackages("com.example.orders")
@IncludeTags("unit")
@ExcludeTags("slow")
class OrderUnitTestSuite { }
@Suite
@SelectClasses({OrderServiceTest.class, PaymentProcessorTest.class})
class SmokeTestSuite { }
Requires:
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>1.11.0</version>
<scope>test</scope>
</dependency>
Temporary Files and Directories
@Test
void shouldWriteReportToFile(@TempDir Path tempDir) {
var reportFile = tempDir.resolve("report.csv");
reporter.write(data, reportFile);
assertThat(reportFile).exists().isNotEmpty();
}
Capturing Output
@Test
void shouldLogWarningForLargeOrders(CapturedOutput output) {
service.place(Order.of("item", 10_000));
assertThat(output).contains("WARN: unusually large quantity");
}
// Requires Spring Boot test slice or OutputCaptureExtension
Ordering Tests
Avoid ordering unless testing stateful workflows. Use @TestMethodOrder only when necessary:
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class CheckoutFlowTest {
@Test @Order(1) void shouldAddItemToCart() { ... }
@Test @Order(2) void shouldApplyCoupon() { ... }
@Test @Order(3) void shouldCompletePurchase() { ... }
}
Reference Guide
| Topic | Recommendation | Validation |
|---|---|---|
| Test structure | Arrange-Act-Assert (AAA) | readable, one concept per test |
| Naming | should<Behavior>When<Condition>() | enforced via convention |
| Isolation | no shared mutable state between tests | parallel run green |
| Coverage | cover happy path, edge cases, and error paths | mutation testing |
| Speed | unit tests < 50ms each | CI timing gates |
| Assertions | prefer AssertJ for readability | code review |
Constraints
MUST DO
- Follow Arrange-Act-Assert layout in every test method
- Test one logical behavior per
@Testmethod - Keep tests independent — no execution order dependencies
- Use
@DisplayNamefor non-obvious test names - Assert on behavior, not on implementation details
MUST NOT DO
- Do not use
Thread.sleep()— use@Timeoutor polling utilities - Do not share mutable fields between test methods without
@BeforeEachreset - Do not skip
@AfterEachcleanup for resources that can leak - Do not test private methods directly — test through the public API
- Do not mix unit tests and integration tests in the same class
Output Templates
## Test Coverage Review
- Class under test: [ClassName]
- Test class: [ClassNameTest]
- Covered behaviors: [list]
- Missing cases: [list]
- Recommended parameterized expansions: [list]
- Lifecycle hooks needed: [BeforeEach/AfterAll/etc.]
// Standard test class template
@Tag("unit")
@DisplayName("OrderService")
class OrderServiceTest {
private OrderService sut;
@BeforeEach
void setUp() {
sut = new OrderService(/* dependencies */);
}
@Nested
@DisplayName("place()")
class Place {
@Test
@DisplayName("should accept a valid order")
void shouldAcceptValidOrder() {
// Arrange
var order = Order.of("item-1", 2);
// Act
var result = sut.place(order);
// Assert
assertThat(result.status()).isEqualTo(OrderStatus.ACCEPTED);
}
@ParameterizedTest(name = "should reject when quantity is {0}")
@ValueSource(ints = {-5, -1, 0})
void shouldRejectNonPositiveQuantity(int qty) {
assertThatThrownBy(() -> sut.place(Order.of("item", qty)))
.isInstanceOf(IllegalArgumentException.class);
}
}
}
Knowledge Reference
junit5, junit jupiter, @test, @parameterizedtest, @nestedtest, @dynamictest, @testfactory, @extendwith, @registerextension, assertj, assertions, parameterized tests, test lifecycle, @beforeeach, @aftereach, @beforeall, @afterall, @tag, @disabled, @timeout, @tempdir, parallel execution, test suites, junit platform, extension model, condition annotations, @enabledonos, @enabledonjre, test ordering, @displayname, @methodsource, @csvsource, @valuesource, @enumsource, maven surefire, gradle junit platform
Related Skills
mockito-experttestcontainers-expertjava-expertcode-reviewer