Testing Spring Boot Applications with Diffblue Cover: Services, Repositories, Controllers
Spring Boot applications have consistent, well-understood patterns: @Service classes orchestrate business logic, @Repository interfaces handle persistence, and @RestController classes expose HTTP endpoints. Diffblue Cover recognizes these patterns and applies appropriate mocking and assertion strategies for each. Here's what to expect from each layer and how to get the most out of the generated tests.
Service Layer
Service classes are where Diffblue Cover performs best. They typically have clearly defined dependencies (injected repositories, other services, external clients), concrete methods with return values, and conditional logic that produces different outcomes.
What Good Service Tests Look Like
Given this service:
@Service
@Transactional
public class AccountService {
private final AccountRepository accountRepository;
private final EmailService emailService;
private final AuditLogger auditLogger;
public AccountService(AccountRepository accountRepository,
EmailService emailService,
AuditLogger auditLogger) {
this.accountRepository = accountRepository;
this.emailService = emailService;
this.auditLogger = auditLogger;
}
public Account activate(Long accountId) {
Account account = accountRepository.findById(accountId)
.orElseThrow(() -> new AccountNotFoundException(accountId));
if (account.getStatus() == AccountStatus.ACTIVE) {
throw new AccountAlreadyActiveException(accountId);
}
account.setStatus(AccountStatus.ACTIVE);
Account saved = accountRepository.save(account);
emailService.sendActivationConfirmation(account.getEmail());
auditLogger.log("ACCOUNT_ACTIVATED", accountId);
return saved;
}
}Diffblue Cover generates tests covering:
@ExtendWith(MockitoExtension.class)
class AccountServiceDiffblueTest {
@Mock
private AccountRepository accountRepository;
@Mock
private AuditLogger auditLogger;
@Mock
private EmailService emailService;
@InjectMocks
private AccountService accountService;
@Test
void testActivate() {
// Arrange
Account account = new Account();
account.setId(1L);
account.setEmail("user@example.com");
account.setStatus(AccountStatus.INACTIVE);
when(accountRepository.findById(1L)).thenReturn(Optional.of(account));
when(accountRepository.save(any(Account.class))).thenReturn(account);
// Act
Account result = accountService.activate(1L);
// Assert
verify(accountRepository).save(any(Account.class));
verify(emailService).sendActivationConfirmation("user@example.com");
verify(auditLogger).log(eq("ACCOUNT_ACTIVATED"), eq(1L));
assertEquals(AccountStatus.ACTIVE, result.getStatus());
}
@Test
void testActivate_accountNotFound_throwsAccountNotFoundException() {
when(accountRepository.findById(99L)).thenReturn(Optional.empty());
assertThrows(AccountNotFoundException.class,
() -> accountService.activate(99L));
}
@Test
void testActivate_alreadyActive_throwsAccountAlreadyActiveException() {
Account account = new Account();
account.setId(1L);
account.setStatus(AccountStatus.ACTIVE);
when(accountRepository.findById(1L)).thenReturn(Optional.of(account));
assertThrows(AccountAlreadyActiveException.class,
() -> accountService.activate(1L));
}
}This is strong output. All three logical paths are covered. The assertions are meaningful, not just "method was called."
What to Review in Service Tests
Check that verify() calls use the right argument matchers. Diffblue sometimes generates verify(repo).save(account) (exact object match) when verify(repo).save(any(Account.class)) is more appropriate and less brittle. If your Account class doesn't override equals(), the exact-match version will break as soon as the object reference changes.
Repository Layer
Spring Data JPA repositories are interfaces. Diffblue Cover generates tests against the interface methods, but the testing strategy depends on whether you're using standard derived queries or custom implementations.
Standard Repository Tests
For a straightforward repository:
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByCustomerIdAndStatus(Long customerId, OrderStatus status);
Optional<Order> findByIdAndDeletedFalse(Long id);
}Diffblue Cover generates unit tests using Mockito mocks of the repository interface — appropriate for testing code that uses the repository, but not for testing the repository itself.
For testing that findByCustomerIdAndStatus produces the correct SQL, you need @DataJpaTest with an in-memory database. Diffblue doesn't generate these by default:
@DataJpaTest
class OrderRepositoryTest {
@Autowired
private OrderRepository orderRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void findByCustomerIdAndStatus_returnsonlyMatchingOrders() {
Order pending = new Order(1L, OrderStatus.PENDING);
Order shipped = new Order(1L, OrderStatus.SHIPPED);
Order otherCustomer = new Order(2L, OrderStatus.PENDING);
entityManager.persist(pending);
entityManager.persist(shipped);
entityManager.persist(otherCustomer);
entityManager.flush();
List<Order> result = orderRepository
.findByCustomerIdAndStatus(1L, OrderStatus.PENDING);
assertEquals(1, result.size());
assertEquals(OrderStatus.PENDING, result.get(0).getStatus());
}
}Write these manually. Diffblue's contribution here is in the service layer tests that mock the repository — it handles those correctly.
Custom Repository Implementations
For @Repository classes with manual JPQL or native queries, Diffblue Cover generates unit tests that mock the EntityManager or JdbcTemplate. The generated tests verify your query-building logic, not query execution. That's appropriate for unit testing; integration tests should cover query correctness.
Controller Layer
REST controllers require more consideration. Diffblue Cover can generate pure unit tests for controllers (mocking the service layer), but Spring MVC semantics — request mapping, serialization, validation — aren't exercised without MockMvc.
What Diffblue Generates by Default
For a controller like:
@RestController
@RequestMapping("/api/accounts")
public class AccountController {
private final AccountService accountService;
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
@PostMapping("/{id}/activate")
public ResponseEntity<AccountDto> activate(@PathVariable Long id) {
Account account = accountService.activate(id);
return ResponseEntity.ok(AccountDto.from(account));
}
@GetMapping("/{id}")
public ResponseEntity<AccountDto> getAccount(@PathVariable Long id) {
return accountService.findById(id)
.map(a -> ResponseEntity.ok(AccountDto.from(a)))
.orElse(ResponseEntity.notFound().build());
}
}Diffblue generates standard unit tests:
@ExtendWith(MockitoExtension.class)
class AccountControllerDiffblueTest {
@Mock
private AccountService accountService;
@InjectMocks
private AccountController accountController;
@Test
void testActivate() {
Account account = new Account();
account.setId(1L);
when(accountService.activate(1L)).thenReturn(account);
ResponseEntity<AccountDto> response = accountController.activate(1L);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
verify(accountService).activate(1L);
}
@Test
void testGetAccount_notFound() {
when(accountService.findById(99L)).thenReturn(Optional.empty());
ResponseEntity<AccountDto> response = accountController.getAccount(99L);
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
}
}These are useful tests. They verify the controller's routing logic and HTTP response codes.
What You Should Add: MockMvc Tests
The generated tests don't verify URL mappings, request body deserialization, or validation annotations (@Valid, @NotNull). Add MockMvc tests for these:
@WebMvcTest(AccountController.class)
class AccountControllerMvcTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private AccountService accountService;
@Test
void activate_validId_returns200() throws Exception {
Account account = new Account();
account.setId(1L);
account.setEmail("user@example.com");
when(accountService.activate(1L)).thenReturn(account);
mockMvc.perform(post("/api/accounts/1/activate"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1L));
}
@Test
void activate_accountNotFound_returns404() throws Exception {
when(accountService.activate(99L))
.thenThrow(new AccountNotFoundException(99L));
mockMvc.perform(post("/api/accounts/99/activate"))
.andExpect(status().isNotFound());
}
}Diffblue Cover does not generate @WebMvcTest tests. Write these yourself for any controller with non-trivial validation or error handling.
Mocking Strategies Diffblue Uses
Diffblue Cover applies these patterns automatically:
Constructor injection → @InjectMocks: When dependencies are constructor-injected (the Spring-recommended pattern), Cover correctly uses @InjectMocks with @Mock fields.
Optional return values: Cover generates both the present and empty cases for Optional-returning methods.
void methods: For void service calls (like auditLogger.log()), Cover uses verify() to assert the call happened, which is the correct assertion strategy.
Exception handling: Cover tests both the path where exceptions are thrown and (when present) the path where they're caught and handled.
Static factory methods: If your entity uses static factories like Order.create(...) instead of constructors, Cover handles these correctly via bytecode analysis.
Configuration and Profiles
Spring Boot applications often have @Profile-annotated beans. Diffblue Cover tests run without a Spring context, so profile-specific configuration doesn't affect test generation. If your service has behavior that differs by profile, Cover won't generate profile-specific tests. Those need to be written manually with @ActiveProfiles.
Recommended Workflow for Spring Boot Projects
- Run
dcover create --module src/main/javaon your service and utility packages first — these yield the best results - Review and commit generated service tests
- Manually write
@DataJpaTesttests for custom repository queries - Manually write
@WebMvcTesttests for controllers with complex validation - Use the generated tests as documentation: they show exactly how Diffblue understood each method's contract
The unit test layer that Diffblue generates tells you that your Spring services behave correctly in isolation. Once the application is deployed, HelpMeTest can run automated scenarios against the live API and UI — verifying that the full request-to-response cycle works as expected, including the parts that unit tests can't reach.