unit-test-service-layer

Unit Testing Service Layer with Mockito

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "unit-test-service-layer" with this command: npx skills add giuseppe-trisciuoglio/developer-kit/giuseppe-trisciuoglio-developer-kit-unit-test-service-layer

Unit Testing Service Layer with Mockito

Overview

This skill provides patterns for unit testing @Service classes using Mockito. It covers mocking all injected dependencies, verifying business logic, testing complex workflows, argument capturing, verification patterns, and testing async/reactive services without starting the Spring container.

When to Use

Use this skill when:

  • Testing business logic in @Service classes

  • Mocking repository and external client dependencies

  • Verifying service interactions with mocked collaborators

  • Testing complex workflows and orchestration logic

  • Want fast, isolated unit tests (no database, no API calls)

  • Testing error handling and edge cases in services

Instructions

Follow these steps to test service layer with Mockito:

  1. Add Testing Dependencies

Include JUnit 5, Mockito, and AssertJ in your test classpath.

  1. Create Test Class with Mockito Extension

Use @ExtendWith(MockitoExtension.class) to enable Mockito annotations.

  1. Declare Mocks and Service Under Test

Use @Mock for dependencies and @InjectMocks for the service being tested.

  1. Arrange Test Data

Create test data objects and configure mock return values using when().thenReturn().

  1. Execute Service Method

Call the service method being tested with test inputs.

  1. Assert Results

Verify the returned value using AssertJ assertions and verify mock interactions.

  1. Test Exception Scenarios

Configure mocks to throw exceptions and verify error handling.

Examples

Setup with Mockito and JUnit 5

Maven

<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <scope>test</scope> </dependency>

Gradle

dependencies { testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core") testImplementation("org.mockito:mockito-junit-jupiter") testImplementation("org.assertj:assertj-core") }

Basic Pattern: Service with Mocked Dependencies

Single Dependency

import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.mockito.Mockito.; import static org.assertj.core.api.Assertions.;

@ExtendWith(MockitoExtension.class) class UserServiceTest {

@Mock private UserRepository userRepository;

@InjectMocks private UserService userService;

@Test void shouldReturnAllUsers() { // Arrange List<User> expectedUsers = List.of( new User(1L, "Alice"), new User(2L, "Bob") ); when(userRepository.findAll()).thenReturn(expectedUsers);

// Act
List&#x3C;User> result = userService.getAllUsers();

// Assert
assertThat(result).hasSize(2);
assertThat(result).containsExactly(
  new User(1L, "Alice"),
  new User(2L, "Bob")
);
verify(userRepository, times(1)).findAll();

} }

Multiple Dependencies

@ExtendWith(MockitoExtension.class) class UserEnrichmentServiceTest {

@Mock private UserRepository userRepository;

@Mock private EmailService emailService;

@Mock private AnalyticsClient analyticsClient;

@InjectMocks private UserEnrichmentService enrichmentService;

@Test void shouldCreateUserAndSendWelcomeEmail() { User newUser = new User(1L, "Alice", "alice@example.com"); when(userRepository.save(any(User.class))).thenReturn(newUser); doNothing().when(emailService).sendWelcomeEmail(newUser.getEmail());

User result = enrichmentService.registerNewUser("Alice", "alice@example.com");

assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getName()).isEqualTo("Alice");

verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail("alice@example.com");
verify(analyticsClient, never()).trackUserRegistration(any());

} }

Testing Exception Handling

Service Throws Expected Exception

@Test void shouldThrowExceptionWhenUserNotFound() { when(userRepository.findById(999L)) .thenThrow(new UserNotFoundException("User not found"));

assertThatThrownBy(() -> userService.getUserDetails(999L)) .isInstanceOf(UserNotFoundException.class) .hasMessageContaining("User not found");

verify(userRepository).findById(999L); }

@Test void shouldRethrowRepositoryException() { when(userRepository.findAll()) .thenThrow(new DataAccessException("Database connection failed"));

assertThatThrownBy(() -> userService.getAllUsers()) .isInstanceOf(DataAccessException.class) .hasMessageContaining("Database connection failed"); }

Testing Complex Workflows

Multiple Service Method Calls

@Test void shouldTransferMoneyBetweenAccounts() { Account fromAccount = new Account(1L, 1000.0); Account toAccount = new Account(2L, 500.0);

when(accountRepository.findById(1L)).thenReturn(Optional.of(fromAccount)); when(accountRepository.findById(2L)).thenReturn(Optional.of(toAccount)); when(accountRepository.save(any(Account.class))) .thenAnswer(invocation -> invocation.getArgument(0));

moneyTransferService.transfer(1L, 2L, 200.0);

// Verify both accounts were updated verify(accountRepository, times(2)).save(any(Account.class)); assertThat(fromAccount.getBalance()).isEqualTo(800.0); assertThat(toAccount.getBalance()).isEqualTo(700.0); }

Argument Capturing and Verification

Capture Arguments Passed to Mock

import org.mockito.ArgumentCaptor;

@Test void shouldCaptureUserDataWhenSaving() { ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class); when(userRepository.save(any(User.class))) .thenAnswer(invocation -> invocation.getArgument(0));

userService.createUser("Alice", "alice@example.com");

verify(userRepository).save(userCaptor.capture()); User capturedUser = userCaptor.getValue();

assertThat(capturedUser.getName()).isEqualTo("Alice"); assertThat(capturedUser.getEmail()).isEqualTo("alice@example.com"); }

@Test void shouldCaptureMultipleArgumentsAcrossMultipleCalls() { ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);

userService.createUser("Alice", "alice@example.com"); userService.createUser("Bob", "bob@example.com");

verify(userRepository, times(2)).save(userCaptor.capture());

List<User> capturedUsers = userCaptor.getAllValues(); assertThat(capturedUsers).hasSize(2); assertThat(capturedUsers.get(0).getName()).isEqualTo("Alice"); assertThat(capturedUsers.get(1).getName()).isEqualTo("Bob"); }

Verification Patterns

Verify Call Order and Frequency

import org.mockito.InOrder;

@Test void shouldCallMethodsInCorrectOrder() { InOrder inOrder = inOrder(userRepository, emailService);

userService.registerNewUser("Alice", "alice@example.com");

inOrder.verify(userRepository).save(any(User.class)); inOrder.verify(emailService).sendWelcomeEmail(any()); }

@Test void shouldCallMethodExactlyOnce() { userService.getUserDetails(1L);

verify(userRepository, times(1)).findById(1L); verify(userRepository, never()).findAll(); }

Testing Async/Reactive Services

Service with CompletableFuture

@Test void shouldReturnCompletableFutureWhenFetchingAsyncData() { List<User> users = List.of(new User(1L, "Alice")); when(userRepository.findAllAsync()) .thenReturn(CompletableFuture.completedFuture(users));

CompletableFuture<List<User>> result = userService.getAllUsersAsync();

assertThat(result).isCompletedWithValue(users); }

Examples

Input: Service Without Test Coverage

@Service public class UserService { private final UserRepository userRepository;

public User getUser(Long id) {
    return userRepository.findById(id).orElse(null);
}

}

Output: Service With Complete Test Coverage

@ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository;

@InjectMocks
private UserService userService;

@Test
void shouldReturnUserWhenFound() {
    User expectedUser = new User(1L, "Alice");
    when(userRepository.findById(1L)).thenReturn(Optional.of(expectedUser));

    User result = userService.getUser(1L);

    assertThat(result).isEqualTo(expectedUser);
    verify(userRepository).findById(1L);
}

@Test
void shouldThrowExceptionWhenNotFound() {
    when(userRepository.findById(999L)).thenReturn(Optional.empty());

    assertThatThrownBy(() -> userService.getUser(999L))
        .isInstanceOf(UserNotFoundException.class);
}

}

Input: Manual Mock Creation (Anti-Pattern)

UserService service = new UserService(new FakeUserRepository());

Output: Mockito-Based Test

@Mock private UserRepository userRepository;

@InjectMocks private UserService userService;

@Test void test() { when(userRepository.findById(1L)).thenReturn(Optional.of(user)); // Test logic }

Best Practices

  • Use @ExtendWith(MockitoExtension.class) for JUnit 5 integration

  • Construct service manually instead of using reflection when possible

  • Mock only direct dependencies of the service under test

  • Verify interactions to ensure correct collaboration

  • Use descriptive variable names: expectedUser , actualUser , captor

  • Test one behavior per test method - keep tests focused

  • Avoid testing framework code - focus on business logic

Common Patterns

Partial Mock with Spy:

@Spy @InjectMocks private UserService userService; // Real instance, but can stub some methods

@Test void shouldUseRealMethodButMockDependency() { when(userRepository.findById(any())).thenReturn(Optional.of(new User())); // Calls real userService methods but userRepository is mocked }

Constructor Injection for Testing:

// In your service (production code) public class UserService { private final UserRepository userRepository;

public UserService(UserRepository userRepository) { this.repository = userRepository; } }

// In your test - can inject mocks directly @Test void test() { UserRepository mockRepo = mock(UserRepository.class); UserService service = new UserService(mockRepo); }

Troubleshooting

UnfinishedStubbingException: Ensure all when() calls are completed with thenReturn() , thenThrow() , or thenAnswer() .

UnnecessaryStubbingException: Remove unused stub definitions. Use @ExtendWith(MockitoExtension.class) with MockitoExtension.LENIENT if you intentionally have unused stubs.

NullPointerException in test: Verify @InjectMocks correctly injects all mocked dependencies into the service constructor.

Constraints and Warnings

  • Do not mock value objects or DTOs; create real instances with test data.

  • Avoid mocking too many dependencies; consider refactoring if a service has too many collaborators.

  • Tests should not rely on execution order; each test must be independent.

  • Be cautious with @Spy as it can lead to partial mocking which is harder to understand.

  • Mock static methods with caution using Mockito-Inline; it can cause memory leaks in long-running test suites.

  • Do not test private methods directly; test them through public method behavior.

  • Argument matchers (any() , eq() ) cannot be mixed with actual values in the same stub.

  • Avoid over-verifying; verify only interactions that are important to the test scenario.

References

  • Mockito Documentation

  • JUnit 5 User Guide

  • AssertJ Assertions

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

shadcn-ui

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

tailwind-css-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

unit-test-bean-validation

No summary provided by upstream source.

Repository SourceNeeds Review