test-quality

Test Quality Skill (JUnit 5 + AssertJ)

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "test-quality" with this command: npx skills add decebals/claude-code-java/decebals-claude-code-java-test-quality

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&#x3C;PluginDescriptor> descriptors = List.of(
        createDescriptor("plugin1"),
        createDescriptor("plugin2")
    );
    when(repository.findAll()).thenReturn(descriptors);
    
    // When
    List&#x3C;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() { }

  1. Implement incrementally
  • One test at a time

  • Verify compilation after each

  • Run tests to validate

  • Refactor if needed

  1. 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

References

  • AssertJ Documentation

  • JUnit 5 User Guide

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

java-code-review

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

clean-code

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

spring-boot-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

concurrency-review

No summary provided by upstream source.

Repository SourceNeeds Review