Implement JUnit 5, Mockito, and AssertJ patterns with 80%+ code coverage for CIA platform unit tests
Write effective, maintainable unit tests using JUnit 5, Mockito, and AssertJ that achieve 80%+ code coverage.
@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");
}
}
@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();
}
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();
}
}
@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"));
}
@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);
}
@Test
void shouldSaveWithCorrectTimestamp() {
ArgumentCaptor<Politician> 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)
);
}
@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<Politician> 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();
}
<!-- pom.xml -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
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
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer