Testing Spring Boot Applications with Diffblue Cover: Services, Repositories, Controllers

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.

  1. Run dcover create --module src/main/java on your service and utility packages first — these yield the best results
  2. Review and commit generated service tests
  3. Manually write @DataJpaTest tests for custom repository queries
  4. Manually write @WebMvcTest tests for controllers with complex validation
  5. 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.

Read more

Start now free