Services are the parts of your code that contain business logic. If controllers are the facade of your app, services are the "brain". Bugs in services can lead to incorrect execution of business processes.
Testing services helps you:
- Verify that the application logic works the way it's supposed to.
- Make sure that all method calls and interactions with dependencies are performed correctly.
- Catch bugs before the app is built and deployed.
Mockito as your best friend
For testing services we'll be using Mockito a lot. Why? Because we want to test business logic in isolation from external dependencies, like databases or external APIs.
Main steps for testing services
- Create mocks for all service dependencies (repositories, other services, etc.).
- Configure mock behavior using
when()andthenReturn(). - Verify that service methods trigger the expected actions and return correct results.
- Optionally ensure dependencies are called the expected number of times (or not called), using methods like
verify().
Practical: testing services
Let's take an example. We have a service that calculates discounts for users. It queries a repository to get user info and contains other app logic.
Service
@Service
public class DiscountService {
private final UserRepository userRepository;
public DiscountService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public double calculateDiscount(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
if (user.isPremium()) {
return 20.0; // 20% discount for premium users
}
return 5.0; // 5% discount for everyone else
}
}
Testing
1. Create the test class and set up mocks:
@ExtendWith(MockitoExtension.class)
class DiscountServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private DiscountService discountService;
// Test data
private final User premiumUser = new User(1L, "PremiumUser", true);
private final User regularUser = new User(2L, "RegularUser", false);
// ...
}
2. Test calculateDiscount() for premium users:
@Test
void shouldReturn20PercentDiscountForPremiumUsers() {
// Set up the mock
when(userRepository.findById(1L)).thenReturn(Optional.of(premiumUser));
// Action
double discount = discountService.calculateDiscount(1L);
// Assertion
assertEquals(20.0, discount);
verify(userRepository, times(1)).findById(1L); // Make sure the repository was called exactly once
}
3. Test calculateDiscount() for regular users:
@Test
void shouldReturn5PercentDiscountForRegularUsers() {
// Set up the mock
when(userRepository.findById(2L)).thenReturn(Optional.of(regularUser));
// Action
double discount = discountService.calculateDiscount(2L);
// Assertion
assertEquals(5.0, discount);
verify(userRepository, times(1)).findById(2L);
}
4. Handle the case when the user is not found:
@Test
void shouldThrowExceptionWhenUserNotFound() {
// Set up the mock
when(userRepository.findById(anyLong())).thenReturn(Optional.empty());
// Check for exception
Exception exception = assertThrows(IllegalArgumentException.class,
() -> discountService.calculateDiscount(99L));
assertEquals("User not found", exception.getMessage());
verify(userRepository, times(1)).findById(99L);
}
Now we're confident that our service works correctly and handles all possible scenarios.
Introduction to testing repositories
Repositories are the bridge between your app and the database. So it's important to make sure they interact with the database properly, running correct queries and handling results.
How to test repositories?
- Use an embedded database, for example H2 (in-memory) for tests.
- The
@DataJpaTestannotation helps auto-configure a test environment for JPA repositories. - Verify that query results match expectations.
Practical: Testing repositories
Entity
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private boolean isPremium;
// Constructors, getters and setters
}
Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findAllByIsPremium(boolean isPremium);
}
Testing
1. Create the test class:
@DataJpaTest
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
// ...
}
2. Test the findAllByIsPremium method:
@Test
void shouldFindAllPremiumUsers() {
// Preparing test data
User premiumUser = new User(null, "PremiumUser", true);
User regularUser = new User(null, "RegularUser", false);
entityManager.persist(premiumUser);
entityManager.persist(regularUser);
entityManager.flush();
// Action
List<User> result = userRepository.findAllByIsPremium(true);
// Assertion
assertEquals(1, result.size());
assertEquals("PremiumUser", result.get(0).getName());
}
3. Test standard JPA methods (for example, findById):
@Test
void shouldFindUserById() {
// Preparing data
User user = new User(null, "TestUser", false);
User savedUser = entityManager.persistFlushFind(user);
// Assertion
Optional<User> result = userRepository.findById(savedUser.getId());
assertTrue(result.isPresent());
assertEquals("TestUser", result.get().getName());
}
Summary
Testing services with Mockito isolates your logic from external dependencies, letting you focus solely on verifying business logic. Testing repositories with @DataJpaTest and an H2 database helps ensure that your queries and data handling are correct.
You've successfully prepared your codebase for real-world tasks, and bugs won't be able to hide in your app anymore! Now you can move on to more advanced testing scenarios, which you'll learn about in the next lectures.
GO TO FULL VERSION