--- name: unit-testing-patterns description: Implement JUnit 5, Mockito, and AssertJ patterns with 80%+ code coverage for CIA platform unit tests license: Apache-2.0 --- # Unit Testing Patterns Skill ## Purpose Write effective, maintainable unit tests using JUnit 5, Mockito, and AssertJ that achieve 80%+ code coverage. ## When to Use - ✅ Testing business logic in services - ✅ Testing data validation - ✅ Testing utility functions - ✅ Test-driven development (TDD) ## JUnit 5 Patterns ### Test Structure (AAA Pattern) ```java @ExtendWith(MockitoExtension.class) class PoliticianServiceTest { @Mock private PoliticianRepository repository; @Mock private AuditLogger auditLogger; @InjectMocks private PoliticianService service; @Test @DisplayName("Should find politician by ID when exists") void shouldFindPoliticianById() { // Arrange String politicianId = "196401011234"; Politician expected = new Politician(politicianId, "John", "Doe"); when(repository.findById(politicianId)).thenReturn(Optional.of(expected)); // Act Politician actual = service.findById(politicianId); // Assert assertThat(actual).isNotNull(); assertThat(actual.getId()).isEqualTo(politicianId); assertThat(actual.getFirstName()).isEqualTo("John"); } @Test @DisplayName("Should throw exception when politician not found") void shouldThrowExceptionWhenNotFound() { // Arrange String politicianId = "999999999999"; when(repository.findById(politicianId)).thenReturn(Optional.empty()); // Act & Assert assertThatThrownBy(() -> service.findById(politicianId)) .isInstanceOf(ResourceNotFoundException.class) .hasMessageContaining("Politician not found"); } } ``` ### Parameterized Tests ```java @ParameterizedTest @CsvSource({ "196401011234, true", "999999999999, true", "12345, false", "abcdefghijkl, false", "'', false" }) @DisplayName("Should validate Swedish personal ID format") void shouldValidatePersonalIdFormat(String personalId, boolean expected) { assertThat(validator.isValidPersonalId(personalId)).isEqualTo(expected); } @ParameterizedTest @ValueSource(strings = {"password", "12345678", "qwerty", "admin"}) @DisplayName("Should reject common passwords") void shouldRejectCommonPasswords(String password) { assertThat(passwordValidator.isWeak(password)).isTrue(); } ``` ### Test Lifecycle ```java class DatabaseTest { @BeforeAll static void setupClass() { // Run once before all tests initializeTestDatabase(); } @BeforeEach void setup() { // Run before each test clearDatabase(); } @AfterEach void teardown() { // Run after each test rollbackTransaction(); } @AfterAll static void teardownClass() { // Run once after all tests closeConnections(); } } ``` ## Mockito Best Practices ### Stubbing ```java @Test void shouldReturnCachedData() { // Stub method to return specific value when(cache.get("key")).thenReturn("value"); // Stub with argument matchers when(repository.findByName(anyString())).thenReturn(new ArrayList<>()); // Stub with custom answer when(service.process(any())).thenAnswer(invocation -> { String arg = invocation.getArgument(0); return arg.toUpperCase(); }); // Stub to throw exception when(repository.findById("invalid")) .thenThrow(new IllegalArgumentException("Invalid ID")); } ``` ### Verification ```java @Test void shouldLogAuditEvent() { // Arrange Politician politician = new Politician("123", "John", "Doe"); // Act service.create(politician); // Verify method was called once verify(auditLogger).log(eq("CREATE_POLITICIAN"), any(Politician.class)); // Verify exact number of calls verify(repository, times(1)).save(politician); // Verify never called verify(emailService, never()).sendNotification(any()); // Verify no more interactions verifyNoMoreInteractions(auditLogger); } ``` ### Argument Captors ```java @Test void shouldSaveWithCorrectTimestamp() { ArgumentCaptor captor = ArgumentCaptor.forClass(Politician.class); service.create(new Politician("123", "John", "Doe")); verify(repository).save(captor.capture()); Politician saved = captor.getValue(); assertThat(saved.getCreatedAt()).isCloseTo( LocalDateTime.now(), within(1, ChronoUnit.SECONDS) ); } ``` ## AssertJ Fluent Assertions ```java @Test void shouldHaveCorrectProperties() { Politician politician = service.findById("123"); // Fluent assertions assertThat(politician) .isNotNull() .hasFieldOrPropertyWithValue("firstName", "John") .hasFieldOrProperty("lastName") .extracting("party") .isEqualTo("S"); // Collection assertions List politicians = service.findAll(); assertThat(politicians) .isNotEmpty() .hasSize(5) .extracting("firstName") .contains("John", "Jane") .doesNotContain("Invalid"); // Exception assertions assertThatThrownBy(() -> service.delete("invalid")) .isInstanceOf(NotFoundException.class) .hasMessage("Politician not found") .hasNoCause(); } ``` ## Code Coverage ### Target: 80% Line Coverage, 70% Branch Coverage ```xml org.jacoco jacoco-maven-plugin ${jacoco.version} prepare-agent report test report check check BUNDLE LINE COVEREDRATIO 0.80 BRANCH COVEREDRATIO 0.70 ``` ## Test Organization ``` src/test/java/ └── com/hack23/cia/ ├── service/ │ ├── impl/ │ │ ├── PoliticianServiceTest.java │ │ └── VotingAnalysisServiceTest.java │ └── api/ │ └── PoliticianServiceIT.java (Integration tests) ├── util/ │ ├── DateUtilsTest.java │ └── ValidationUtilsTest.java └── testfoundation/ ├── TestDataFactory.java └── AbstractServiceTest.java ``` ## References - JUnit 5: https://junit.org/junit5/ - Mockito: https://site.mockito.org/ - AssertJ: https://assertj.github.io/doc/ - UnitTestPlan.md