Claude-code-java test-quality
Write high-quality JUnit 5 tests with AssertJ assertions. Use when user says "add tests", "write tests", "improve test coverage", or when reviewing/creating test classes for Java code.
install
source · Clone the upstream repo
git clone https://github.com/decebals/claude-code-java
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/decebals/claude-code-java "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/test-quality" ~/.claude/skills/decebals-claude-code-java-test-quality && rm -rf "$T"
manifest:
.claude/skills/test-quality/SKILL.mdsource content
Test Quality Skill (JUnit 5 + AssertJ)
Write high-quality, maintainable tests for Java projects using modern best practices.
When to Use
- Writing new test classes
- Reviewing/improving existing tests
- User asks to "add tests" / "improve test coverage"
- Code review mentions missing tests
Framework Preferences
JUnit 5 (Jupiter)
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import static org.assertj.core.api.Assertions.*;
AssertJ over standard assertions
✅ Use AssertJ:
assertThat(plugin.getState()) .as("Plugin should be started after initialization") .isEqualTo(PluginState.STARTED); assertThat(plugins) .hasSize(3) .extracting(Plugin::getId) .containsExactly("plugin1", "plugin2", "plugin3");
❌ Avoid JUnit assertions:
assertEquals(PluginState.STARTED, plugin.getState()); // Less readable assertTrue(plugins.size() == 3); // Less descriptive failures
Test Structure (AAA Pattern)
Always use Arrange-Act-Assert pattern:
@Test @DisplayName("Should load plugin from valid directory") void shouldLoadPluginFromValidDirectory() { // Arrange - Setup test data and dependencies Path pluginDir = Paths.get("test-plugins/valid-plugin"); PluginLoader loader = new DefaultPluginLoader(); // Act - Execute the behavior being tested Plugin plugin = loader.load(pluginDir); // Assert - Verify results assertThat(plugin) .isNotNull() .extracting(Plugin::getId, Plugin::getVersion) .containsExactly("test-plugin", "1.0.0"); }
Naming Conventions
Test class names
// Class under test: PluginManager PluginManagerTest // ✅ Simple, standard PluginManagerShould // ✅ BDD style (if team prefers) TestPluginManager // ❌ Avoid
Test method names
Option 1: should_expectedBehavior_when_condition (descriptive)
@Test void should_throwException_when_pluginDirectoryNotFound() { } @Test void should_returnEmptyList_when_noPluginsAvailable() { } @Test void should_loadPluginsInDependencyOrder_when_multipleDependencies() { }
Option 2: Natural language with @DisplayName (cleaner code)
@Test @DisplayName("Should load all plugins from directory") void loadAllPlugins() { } @Test @DisplayName("Should throw exception when plugin descriptor is invalid") void invalidPluginDescriptor() { }
AssertJ Power Features
Collection assertions
// Basic collection checks assertThat(plugins) .isNotEmpty() .hasSize(2) .doesNotContainNull(); // Advanced filtering and extraction assertThat(plugins) .filteredOn(p -> p.getState() == PluginState.STARTED) .extracting(Plugin::getId) .containsExactlyInAnyOrder("plugin-a", "plugin-b"); // All elements match condition assertThat(plugins) .allMatch(p -> p.getVersion() != null, "All plugins have version");
Exception assertions
// Basic exception check assertThatThrownBy(() -> loader.load(invalidPath)) .isInstanceOf(PluginException.class) .hasMessageContaining("Invalid plugin descriptor"); // Detailed exception verification assertThatThrownBy(() -> manager.startPlugin("missing-plugin")) .isInstanceOf(PluginException.class) .hasMessageContaining("Plugin not found") .hasCauseInstanceOf(IllegalArgumentException.class) .hasNoCause(); // or verify cause chain // With assertThatExceptionOfType (more readable) assertThatExceptionOfType(PluginException.class) .isThrownBy(() -> loader.load(invalidPath)) .withMessageContaining("Invalid") .withMessageMatching("Invalid .* descriptor");
Object assertions
// Extract and verify multiple properties assertThat(plugin) .isNotNull() .extracting("id", "version", "state") .containsExactly("my-plugin", "1.0", PluginState.STARTED); // Using method references (type-safe) assertThat(plugin) .extracting(Plugin::getId, Plugin::getVersion, Plugin::getState) .containsExactly("my-plugin", "1.0", PluginState.STARTED); // Field by field comparison assertThat(actualPlugin) .usingRecursiveComparison() .isEqualTo(expectedPlugin);
Soft assertions (multiple checks)
@Test void shouldHaveValidPluginDescriptor() { SoftAssertions softly = new SoftAssertions(); softly.assertThat(descriptor.getId()) .as("Plugin ID") .isNotBlank() .matches("[a-z0-9-]+"); softly.assertThat(descriptor.getVersion()) .as("Plugin version") .matches("\\d+\\.\\d+\\.\\d+"); softly.assertThat(descriptor.getDependencies()) .as("Dependencies") .isNotNull() .doesNotContainNull(); softly.assertAll(); // All assertions evaluated, even if some fail }
String assertions
assertThat(errorMessage) .startsWith("Error:") .contains("plugin", "failed") .doesNotContain("success") .matches("Error: .* failed") .hasLineCount(3);
Test Organization
Nested tests for clarity
@DisplayName("PluginManager") class PluginManagerTest { private PluginManager manager; @BeforeEach void setUp() { manager = new DefaultPluginManager(); } @Nested @DisplayName("when starting plugins") class WhenStartingPlugins { @Test @DisplayName("should start all plugins in dependency order") void shouldStartInDependencyOrder() { // Test implementation } @Test @DisplayName("should skip disabled plugins") void shouldSkipDisabledPlugins() { // Test implementation } @Test @DisplayName("should fail if circular dependency detected") void shouldFailOnCircularDependency() { // Test implementation } } @Nested @DisplayName("when stopping plugins") class WhenStoppingPlugins { @Test @DisplayName("should stop plugins in reverse dependency order") void shouldStopInReverseOrder() { // Test implementation } } }
Parameterized tests
@ParameterizedTest @ValueSource(strings = {"1.0.0", "2.1.3", "10.0.0-SNAPSHOT"}) @DisplayName("Should accept valid semantic versions") void shouldAcceptValidVersions(String version) { assertThat(VersionParser.parse(version)) .isNotNull() .hasFieldOrPropertyWithValue("valid", true); } @ParameterizedTest @CsvSource({ "plugin-a, 1.0, STARTED", "plugin-b, 2.0, STOPPED", "plugin-c, 1.5, DISABLED" }) @DisplayName("Should load plugin with expected state") void shouldLoadPluginWithState(String id, String version, PluginState expectedState) { Plugin plugin = createPlugin(id, version); assertThat(plugin.getState()).isEqualTo(expectedState); } @ParameterizedTest @MethodSource("invalidPluginDescriptors") @DisplayName("Should reject invalid plugin descriptors") void shouldRejectInvalidDescriptors(PluginDescriptor descriptor, String expectedError) { assertThatThrownBy(() -> validator.validate(descriptor)) .hasMessageContaining(expectedError); } static Stream<Arguments> invalidPluginDescriptors() { return Stream.of( Arguments.of(descriptorWithoutId(), "Missing plugin ID"), Arguments.of(descriptorWithInvalidVersion(), "Invalid version format"), Arguments.of(descriptorWithEmptyId(), "Plugin ID cannot be empty") ); }
Common Patterns
Testing with mocks (Mockito)
@ExtendWith(MockitoExtension.class) class PluginManagerTest { @Mock private PluginRepository repository; @Mock private PluginValidator validator; @InjectMocks private DefaultPluginManager manager; @Test @DisplayName("Should load plugins from repository") void shouldLoadPluginsFromRepository() { // Given List<PluginDescriptor> descriptors = List.of( createDescriptor("plugin1"), createDescriptor("plugin2") ); when(repository.findAll()).thenReturn(descriptors); // When List<Plugin> plugins = manager.loadAll(); // Then assertThat(plugins).hasSize(2); verify(repository).findAll(); verify(validator, times(2)).validate(any(PluginDescriptor.class)); } }
Test fixtures with @BeforeEach
@BeforeEach void setUp() throws IOException { // Create temporary directory for test plugins pluginDir = Files.createTempDirectory("test-plugins"); // Initialize plugin manager with test config PluginConfig config = PluginConfig.builder() .pluginDirectory(pluginDir) .enableValidation(true) .build(); pluginManager = new DefaultPluginManager(config); } @AfterEach void tearDown() throws IOException { // Clean up test resources if (pluginManager != null) { pluginManager.stopAll(); } if (pluginDir != null) { FileUtils.deleteDirectory(pluginDir.toFile()); } }
Testing async operations
@Test @DisplayName("Should complete async plugin loading") void shouldCompleteAsyncLoading() { CompletableFuture<Plugin> future = manager.loadAsync(pluginPath); assertThat(future) .succeedsWithin(Duration.ofSeconds(5)) .satisfies(plugin -> { assertThat(plugin.getState()).isEqualTo(PluginState.STARTED); assertThat(plugin.getId()).isNotBlank(); }); }
Token Optimization
When writing tests:
1. Generate test skeleton first
// Phase 1: List test cases as comments // @Test void shouldLoadPlugin() { } // @Test void shouldThrowExceptionForInvalidPlugin() { } // @Test void shouldHandleMissingDependencies() { }
2. Implement incrementally
- One test at a time
- Verify compilation after each
- Run tests to validate
- Refactor if needed
3. Reuse patterns
// Extract common setup to helper methods private Plugin createTestPlugin(String id, String version) { return Plugin.builder() .id(id) .version(version) .build(); }
Code Coverage Guidelines
- Aim for: 80%+ line coverage on core logic
- Focus on: Business logic, complex algorithms, edge cases
- Skip: Trivial getters/setters, POJOs, generated code
- Test: Happy paths + error conditions + boundary cases
What to test
✅ High priority:
- Public APIs
- Complex business logic
- Error handling
- Edge cases and boundaries
- Integration points
❌ Low priority:
// Simple getters/setters public String getId() { return id; } public void setId(String id) { this.id = id; } // Simple POJOs with no logic public class PluginInfo { private String id; private String version; // ... only getters/setters }
Anti-patterns
❌ Avoid:
// 1. Generic test names @Test void test1() { } @Test void testPlugin() { } // 2. Testing implementation details assertThat(plugin.internalState.flag).isTrue(); // Couples to internals // 3. Brittle assertions with timestamps assertThat(message).isEqualTo("Error at 2024-01-26 10:30:15"); // 4. Multiple unrelated assertions @Test void testEverything() { // 50 unrelated assertions assertThat(plugin.getId()).isNotNull(); assertThat(manager.getCount()).isEqualTo(5); assertThat(config.isEnabled()).isTrue(); // ... mixing multiple concerns } // 5. Ignoring exceptions @Test void shouldFail() { try { loader.load(invalidPath); fail("Should have thrown exception"); } catch (Exception e) { // Swallowing exception details } }
✅ Prefer:
@Test @DisplayName("Should reject plugin with missing dependencies") void shouldRejectPluginWithMissingDependencies() { PluginDescriptor descriptor = PluginDescriptor.builder() .id("test-plugin") .dependencies(List.of("missing-dep")) .build(); assertThatThrownBy(() -> manager.load(descriptor)) .isInstanceOf(PluginException.class) .hasMessageContaining("Missing dependencies: missing-dep"); }
Integration with Coverage Tools
Maven configuration
<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</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>
After test generation, suggest:
# Run tests with coverage mvn clean test jacoco:report # View coverage report open target/site/jacoco/index.html # Check coverage threshold mvn verify # Fails if below configured threshold
Quick Reference
// ===== Basic Assertions ===== assertThat(value).isEqualTo(expected); assertThat(value).isNotNull(); assertThat(value).isInstanceOf(String.class); assertThat(number).isPositive().isGreaterThan(5); // ===== Collections ===== assertThat(list).hasSize(3); assertThat(list).contains(item); assertThat(list).containsExactly(item1, item2, item3); assertThat(list).containsExactlyInAnyOrder(item2, item1, item3); assertThat(list).doesNotContain(item); assertThat(list).allMatch(predicate); // ===== Strings ===== assertThat(str).isNotBlank(); assertThat(str).startsWith("prefix"); assertThat(str).endsWith("suffix"); assertThat(str).contains("substring"); assertThat(str).matches("regex\\d+"); // ===== Exceptions ===== assertThatThrownBy(() -> code()) .isInstanceOf(PluginException.class) .hasMessageContaining("error"); assertThatNoException().isThrownBy(() -> code()); // ===== Custom Descriptions ===== assertThat(userId) .as("User ID should be positive") .isPositive(); // ===== Object Comparison ===== assertThat(actual) .usingRecursiveComparison() .ignoringFields("timestamp", "id") .isEqualTo(expected);
Best Practices Summary
- Use AssertJ for all assertions
- Follow AAA pattern (Arrange-Act-Assert)
- Descriptive names with @DisplayName
- One concept per test
- Test behavior, not implementation
- Extract helpers for common setup
- Use @Nested for logical grouping
- Parameterize similar tests
- Soft assertions for multiple checks
- Coverage on business logic, not boilerplate