unit-test-service-layer

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Unit Testing Service Layer with Mockito

使用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.
本技能提供使用Mockito对@Service类进行单元测试的模式,涵盖模拟所有注入依赖项、验证业务逻辑、测试复杂工作流、参数捕获、验证模式,以及无需启动Spring容器即可测试异步/响应式服务。

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
在以下场景下使用本技能:
  • 测试@Service类中的业务逻辑
  • 模拟仓库(Repository)和外部客户端依赖
  • 验证服务与模拟协作对象的交互
  • 测试复杂工作流与编排逻辑
  • 需要快速、独立的单元测试(无需数据库、无需API调用)
  • 测试服务中的错误处理与边缘情况

Instructions

操作步骤

Follow these steps to test service layer with Mockito:
按照以下步骤使用Mockito测试服务层:

1. Add Testing Dependencies

1. 添加测试依赖

Include JUnit 5, Mockito, and AssertJ in your test classpath.
在测试类路径中包含JUnit 5、Mockito和AssertJ。

2. Create Test Class with Mockito Extension

2. 创建带有Mockito扩展的测试类

Use @ExtendWith(MockitoExtension.class) to enable Mockito annotations.
使用@ExtendWith(MockitoExtension.class)启用Mockito注解。

3. Declare Mocks and Service Under Test

3. 声明模拟对象与待测试服务

Use @Mock for dependencies and @InjectMocks for the service being tested.
使用@Mock标记依赖项,使用@InjectMocks标记待测试的服务。

4. Arrange Test Data

4. 准备测试数据

Create test data objects and configure mock return values using when().thenReturn().
创建测试数据对象,并使用when().thenReturn()配置模拟对象的返回值。

5. Execute Service Method

5. 执行服务方法

Call the service method being tested with test inputs.
调用待测试的服务方法并传入测试输入。

6. Assert Results

6. 断言结果

Verify the returned value using AssertJ assertions and verify mock interactions.
使用AssertJ断言验证返回值,并验证模拟对象的交互情况。

7. Test Exception Scenarios

7. 测试异常场景

Configure mocks to throw exceptions and verify error handling.
配置模拟对象抛出异常,验证错误处理逻辑。

Examples

示例

Setup with Mockito and JUnit 5

Mockito与JUnit 5的配置

Maven

Maven

xml
<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>
xml
<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

Gradle

kotlin
dependencies {
  testImplementation("org.junit.jupiter:junit-jupiter")
  testImplementation("org.mockito:mockito-core")
  testImplementation("org.mockito:mockito-junit-jupiter")
  testImplementation("org.assertj:assertj-core")
}
kotlin
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

单一依赖

java
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<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();
  }
}
java
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() {
    // 准备
    List<User> expectedUsers = List.of(
      new User(1L, "Alice"),
      new User(2L, "Bob")
    );
    when(userRepository.findAll()).thenReturn(expectedUsers);

    // 执行
    List<User> result = userService.getAllUsers();

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

Multiple Dependencies

多依赖

java
@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());
  }
}
java
@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

服务抛出预期异常

java
@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");
}
java
@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

多服务方法调用

java
@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);
}
java
@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(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

捕获传递给模拟对象的参数

java
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");
}
java
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

调用顺序与频率验证

java
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();
}
java
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

带有CompletableFuture的服务

java
@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);
}
java
@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

输入:无测试覆盖的服务

java
@Service
public class UserService {
    private final UserRepository userRepository;

    public User getUser(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}
java
@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

输出:具备完整测试覆盖的服务

java
@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);
    }
}
java
@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)

输入:手动创建模拟对象(反模式)

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

Output: Mockito-Based Test

输出:基于Mockito的测试

java
@Mock
private UserRepository userRepository;

@InjectMocks
private UserService userService;

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

@InjectMocks
private UserService userService;

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

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
  • 使用@ExtendWith(MockitoExtension.class) 实现JUnit 5集成
  • 尽可能手动构造服务,而非使用反射
  • 仅模拟服务的直接依赖项
  • 验证交互情况,确保协作正确
  • 使用描述性变量名:如expectedUser、actualUser、captor
  • 每个测试方法测试一种行为 - 保持测试聚焦
  • 避免测试框架代码 - 专注于业务逻辑

Common Patterns

常见模式

Partial Mock with Spy:
java
@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:
java
// 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);
}
使用Spy进行部分模拟:
java
@Spy
@InjectMocks
private UserService userService; // 真实实例,但可以存根部分方法

@Test
void shouldUseRealMethodButMockDependency() {
  when(userRepository.findById(any())).thenReturn(Optional.of(new User()));
  // 调用真实的userService方法,但userRepository是模拟的
}
用于测试的构造函数注入:
java
// 在你的服务(生产代码)中
public class UserService {
  private final UserRepository userRepository;
  
  public UserService(UserRepository userRepository) {
    this.repository = userRepository;
  }
}

// 在你的测试中 - 可以直接注入模拟对象
@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.
UnfinishedStubbingException:确保所有
when()
调用都通过
thenReturn()
thenThrow()
thenAnswer()
完成配置。
UnnecessaryStubbingException:移除未使用的存根定义。如果有意保留未使用的存根,可以结合
@ExtendWith(MockitoExtension.class)
MockitoExtension.LENIENT
测试中出现NullPointerException:验证
@InjectMocks
是否正确将所有模拟依赖项注入到服务构造函数中。

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.
  • 不要模拟值对象或DTO;使用测试数据创建真实实例。
  • 避免模拟过多依赖项;如果服务有太多协作对象,考虑重构。
  • 测试不应依赖执行顺序;每个测试必须独立。
  • 谨慎使用
    @Spy
    ,因为它会导致部分模拟,难以理解。
  • 谨慎使用Mockito-Inline模拟静态方法;这可能在长期运行的测试套件中导致内存泄漏。
  • 不要直接测试私有方法;通过公共方法的行为来测试它们。
  • 参数匹配器(
    any()
    eq()
    )不能在同一个存根中与实际值混合使用。
  • 避免过度验证;仅验证对测试场景重要的交互。

References

参考资料