Mockito应用
# 一、概述
# 1. 什么是Mockito
Mockito 是一个流行的 Java 单元测试框架,用于创建和配置 mock 对象。
它通过模拟对象的行为,帮助开发者编写更加可靠和可维护的单元测试。
Mockito 提供了简单易用的 API,使得测试代码更简洁和直观。
# 2. Mockito的优势
- 简单易用:Mockito 的 API 设计非常直观,易于理解和使用。
- 强大的功能:支持多种方式来设置 mock 对象的行为,包括返回值、异常抛出等。
- 良好的社区支持:拥有活跃的社区和丰富的文档资源,容易找到问题的解决方案。
- 集成性好:可以轻松地与各种构建工具(如 Maven 和 Gradle)和 IDE(如 IntelliJ IDEA 和 Eclipse)集成。
- 性能优化:Mockito 的实现经过了性能优化,确保在大规模测试中也能保持高效。
# 3. Mockito的适用场景
- 单元测试:Mockito 最常用于单元测试中,通过模拟依赖对象来隔离被测代码,从而更容易进行测试。
- 复杂业务逻辑:当业务逻辑涉及多个服务或组件时,使用 Mockito 可以模拟这些服务或组件,使测试更加集中于被测功能。
- 外部系统集成:在需要测试与外部系统的交互时,Mockito 可以模拟这些外部系统的行为,避免对外部系统的实际调用。
- 多线程环境:虽然 Mockito 主要用于单线程测试,但在某些情况下也可以用于测试多线程环境中的行为。
- Spring 应用程序:在 Spring 框架中,Mockito 可以与
@MockBean
和@SpyBean
等注解结合使用,方便地进行 Spring 组件的测试。
# 二、安装与配置
# 1. 添加依赖
在使用 Mockito 之前,需要在项目的构建配置文件中添加相应的依赖。以下是 Maven 和 Gradle 的配置示例。
# 1.1 Maven配置
在 pom.xml
文件中添加以下依赖:
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>LATEST</version>
<scope>test</scope>
</dependency>
</dependencies>
# 1.2 Gradle配置
在 build.gradle
文件中添加以下依赖:
dependencies {
testImplementation 'org.mockito:mockito-all:LATEST'
}
# 三、基础用法
# 1. 创建Mock对象
在 Mockito 中,创建 mock 对象有两种主要方式:使用 @Mock
注解和 Mockito.mock()
方法。
# 1.1 使用 @Mock
注解
@Mock
注解用于字段注入,通常与 @RunWith(MockitoJUnitRunner.class)
(适用于Junit4) 或 @ExtendWith(MockitoExtension.class)
(适用于Junit5) 一起使用。
以下是一个示例:
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(MockitoExtension.class)
public class MyTest {
@Mock
private MyService myService;
@Test
public void testMethod() {
// 你可以直接使用 myService 进行测试
when(myService.doSomething()).thenReturn("mocked result");
String result = myService.doSomething();
assertEquals("mocked result", result);
}
}
# 1.2 使用 Mockito.mock()
方法
Mockito.mock()
方法用于手动创建 mock 对象。以下是一个示例:
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
when(myService.doSomething()).thenReturn("mocked result");
String result = myService.doSomething();
assertEquals("mocked result", result);
}
}
# 2. 设置行为
Mockito 提供了多种方法来设置 mock 对象的行为。以下是一些常用的方法。
# 2.1 when-thenReturn
when-thenReturn
是最常用的设置 mock 对象返回值的方法。
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
when(myService.doSomething()).thenReturn("mocked result");
String result = myService.doSomething();
assertEquals("mocked result", result);
}
}
# 2.2 doReturn-when
doReturn-when
用于处理更复杂的情况,如返回值依赖于方法参数或需要模拟 void 方法的行为。
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
// 模拟有参数的方法
doReturn("mocked result").when(myService).doSomething("param");
String result = myService.doSomething("param");
assertEquals("mocked result", result);
// 模拟 void 方法
doNothing().when(myService).doSomethingVoid();
myService.doSomethingVoid();
verify(myService).doSomethingVoid();
}
}
# 2.3 doThrow-when
doThrow-when
用于模拟方法抛出异常的情况。
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
// 模拟方法抛出异常
doThrow(new RuntimeException("Expected exception")).when(myService).doSomething();
try {
myService.doSomething();
fail("Expected exception was not thrown");
} catch (RuntimeException e) {
assertEquals("Expected exception", e.getMessage());
}
}
}
# 3. 验证行为
验证 mock 对象的行为是确保测试覆盖的一个重要步骤。以下是一些常用的验证方法。
# 3.1 verify()
verify()
用于验证 mock 对象的方法是否被调用。
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
myService.doSomething();
// 验证方法被调用一次
verify(myService, times(1)).doSomething();
}
}
# 3.2 verifyNoInteractions()
verifyNoInteractions()
用于验证某个 mock 对象没有被调用任何方法。
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
// 确保 myService 没有任何方法被调用
verifyNoInteractions(myService);
}
}
# 3.3 verifyZeroInteractions()
verifyZeroInteractions()
用于验证一个或多个 mock 对象没有任何方法被调用。
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService1 = mock(MyService.class);
MyService myService2 = mock(MyService.class);
// 确保 myService1 和 myService2 都没有任何方法被调用
verifyZeroInteractions(myService1, myService2);
}
}
通过这些基本用法,你可以开始使用 Mockito 进行单元测试,并确保你的代码按预期工作。
# 四、进阶技巧
# 1. Stubbing
Stubbing 是 Mockito 中用于定义 mock 对象行为的一种方式。通过 stubbing,你可以指定方法的返回值、抛出异常或进行复杂的操作。
# 1.1 返回值
你可以使用 thenReturn
来设置方法的返回值。
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
when(myService.doSomething()).thenReturn("mocked result");
String result = myService.doSomething();
assertEquals("mocked result", result);
}
}
# 1.2 异常抛出
使用 thenThrow
可以模拟方法抛出异常的情况。
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
when(myService.doSomething()).thenThrow(new RuntimeException("Expected exception"));
try {
myService.doSomething();
fail("Expected exception was not thrown");
} catch (RuntimeException e) {
assertEquals("Expected exception", e.getMessage());
}
}
}
# 1.3 连续调用
可以使用 thenReturn
和 thenThrow
的组合来模拟多次调用时的不同行为。
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
when(myService.doSomething())
.thenReturn("first call")
.thenReturn("second call")
.thenThrow(new RuntimeException("Expected exception"));
assertEquals("first call", myService.doSomething());
assertEquals("second call", myService.doSomething());
try {
myService.doSomething();
fail("Expected exception was not thrown");
} catch (RuntimeException e) {
assertEquals("Expected exception", e.getMessage());
}
}
}
# 2. ArgumentMatchers
ArgumentMatchers 用于匹配传递给 mock 方法的参数。这在处理复杂参数时非常有用。
# 2.1 任意参数
使用 any()
匹配任意类型的参数。
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
when(myService.doSomething(any(String.class))).thenReturn("mocked result");
String result = myService.doSomething("any string");
assertEquals("mocked result", result);
}
}
# 2.2 特定参数
使用 eq()
或 argThat()
匹配特定参数。
import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
when(myService.doSomething(eq("specific value"))).thenReturn("mocked result");
String result = myService.doSomething("specific value");
assertEquals("mocked result", result);
// 使用自定义匹配器
when(myService.doSomething(argThat(s -> s.length() > 5))).thenReturn("mocked result for long string");
String longResult = myService.doSomething("long string");
assertEquals("mocked result for long string", longResult);
}
}
# 2.3 参数捕获器
使用 ArgumentCaptor
捕获传递给方法的参数,以便后续验证。
import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
myService.doSomething("capture this argument");
ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
verify(myService).doSomething(argumentCaptor.capture());
String capturedArgument = argumentCaptor.getValue();
assertEquals("capture this argument", capturedArgument);
}
}
# 3. Spies
Spies 是部分模拟的对象,允许你在不完全模拟对象的情况下对其进行测试。
# 3.1 创建Spy
使用 spy()
方法创建 spy 对象。
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService realObject = new MyServiceImpl();
MyService spyObject = spy(realObject);
// 调用实际方法
String result = spyObject.doSomething();
assertEquals("real result", result);
// 模拟方法
doReturn("mocked result").when(spyObject).doSomething();
String mockedResult = spyObject.doSomething();
assertEquals("mocked result", mockedResult);
}
}
# 3.2 使用Spy
使用 spy 对象时,你可以选择性地模拟某些方法,而其他方法仍然调用真实实现。
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService realObject = new MyServiceImpl();
MyService spyObject = spy(realObject);
// 调用实际方法
String realResult = spyObject.doSomething();
assertEquals("real result", realResult);
// 模拟方法
doReturn("mocked result").when(spyObject).doSomething();
String mockedResult = spyObject.doSomething();
assertEquals("mocked result", mockedResult);
// 验证方法被调用
verify(spyObject, times(2)).doSomething();
}
}
# 4. 自定义Answer
当你需要更复杂的逻辑来控制方法的行为时,可以使用自定义 Answer。
# 4.1 使用 thenAnswer
使用 thenAnswer
来定义一个自定义的行为。
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
when(myService.doSomething(anyString()))
.thenAnswer(invocation -> {
String arg = invocation.getArgument(0);
return "Processed: " + arg.toUpperCase();
});
String result = myService.doSomething("hello");
assertEquals("Processed: HELLO", result);
}
}
# 4.2 实现 Answer
接口
你也可以实现 org.mockito.stubbing.Answer
接口来定义更复杂的行为。
import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void testMethod() {
MyService myService = mock(MyService.class);
Answer<String> customAnswer = invocation -> {
String arg = invocation.getArgument(0);
return "Processed: " + arg.toUpperCase();
};
when(myService.doSomething(anyString())).thenAnswer(customAnswer);
String result = myService.doSomething("hello");
assertEquals("Processed: HELLO", result);
}
}
# 5. 验证顺序
在某些情况下,我们不仅需要验证某个方法是否被调用,还需要验证这些方法的调用顺序。Mockito 提供了 inOrder
方法来帮助我们验证方法调用的顺序。
# 5.1 使用 inOrder
验证顺序
假设我们有一个 OrderService
,它需要依次调用 processPayment
和 saveOrder
方法。
public class OrderService {
private PaymentService paymentService;
private OrderRepository orderRepository;
public OrderService(PaymentService paymentService, OrderRepository orderRepository) {
this.paymentService = paymentService;
this.orderRepository = orderRepository;
}
public boolean placeOrder(Order order) {
if (paymentService.processPayment(order.getAmount())) {
orderRepository.save(order);
return true;
}
return false;
}
}
编写测试类:
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class OrderServiceTest {
@Mock
private PaymentService paymentService;
@Mock
private OrderRepository orderRepository;
@InjectMocks
private OrderService orderService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testPlaceOrder_Success() {
// Arrange
Order order = new Order(1L, 100.0);
when(paymentService.processPayment(order.getAmount())).thenReturn(true);
// Act
boolean result = orderService.placeOrder(order);
// Assert
assertTrue(result);
InOrder inOrder = inOrder(paymentService, orderRepository);
inOrder.verify(paymentService).processPayment(order.getAmount());
inOrder.verify(orderRepository).save(order);
}
}
# 6. 重置Mock对象
有时候我们需要在多个测试方法之间重置 mock 对象的状态,以确保每个测试方法之间的独立性。Mockito 提供了 reset
方法来实现这一点。
# 6.1 使用 reset
重置Mock对象
假设我们有一个 UserService
,并且我们需要在两个测试方法之间重置 UserRepository
的状态。
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
public boolean deleteUserById(Long id) {
return userRepository.deleteById(id);
}
}
编写测试类:
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testGetUserById() {
// Arrange
Long userId = 1L;
User user = new User(userId, "John Doe");
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
// Act
User result = userService.getUserById(userId);
// Assert
assertNotNull(result);
assertEquals("John Doe", result.getName());
// 重置mock对象
reset(userRepository);
}
@Test
void testDeleteUserById() {
// Arrange
Long userId = 1L;
when(userRepository.deleteById(userId)).thenReturn(true);
// Act
boolean result = userService.deleteUserById(userId);
// Assert
assertTrue(result);
verify(userRepository).deleteById(userId);
}
}
# 7. Mock静态方法
Mockito 从版本 3.4.0 开始引入了 MockedStatic
接口,使得模拟静态方法变得更加简单和直观。我们可以通过 Mockito.mockStatic
方法来创建一个 MockedStatic
对象,从而在测试中模拟静态方法的行为。
# 7.1 使用 MockedStatic
模拟静态方法
假设我们有一个 Utils
类,其中包含一个静态方法 getSystemTime
。
public class Utils {
public static long getSystemTime() {
return System.currentTimeMillis();
}
}
我们需要模拟这个静态方法的行为,并在单元测试中进行验证。
然后,编写测试类:
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
public class UtilsTest {
@Test
void testGetSystemTime() {
// Arrange
long mockTime = 1633072800000L; // 2021-10-01 00:00:00
try (MockedStatic<Utils> utilsMock = Mockito.mockStatic(Utils.class)) {
utilsMock.when(Utils::getSystemTime).thenReturn(mockTime);
// Act
long result = Utils.getSystemTime();
// Assert
assertEquals(mockTime, result);
}
}
@Test
void testMultipleCallsToGetSystemTime() {
// Arrange
long mockTime1 = 1633072800000L; // 2021-10-01 00:00:00
long mockTime2 = 1633076400000L; // 2021-10-01 01:00:00
try (MockedStatic<Utils> utilsMock = Mockito.mockStatic(Utils.class)) {
utilsMock.when(Utils::getSystemTime)
.thenReturn(mockTime1)
.thenReturn(mockTime2);
// Act
long result1 = Utils.getSystemTime();
long result2 = Utils.getSystemTime();
// Assert
assertEquals(mockTime1, result1);
assertEquals(mockTime2, result2);
}
}
@Test
void testVerifyCallToGetSystemTime() {
// Arrange
long mockTime = 1633072800000L; // 2021-10-01 00:00:00
try (MockedStatic<Utils> utilsMock = Mockito.mockStatic(Utils.class)) {
utilsMock.when(Utils::getSystemTime).thenReturn(mockTime);
// Act
long result = Utils.getSystemTime();
// Assert
assertEquals(mockTime, result);
utilsMock.verify(() -> Utils.getSystemTime());
}
}
}
# 7.2 解释
MockedStatic<Utils>
: 创建一个MockedStatic
对象,用于模拟Utils
类的静态方法。utilsMock.when(Utils::getSystemTime).thenReturn(mockTime)
: 定义当调用Utils.getSystemTime
方法时返回mockTime
。try-with-resources
:MockedStatic
实现了AutoCloseable
接口,因此可以使用try-with-resources
语句来自动关闭它,恢复原始的静态方法行为。utilsMock.verify(() -> Utils.getSystemTime())
: 验证Utils.getSystemTime
方法是否被调用。
# 7.3 使用限制
虽然 MockedStatic
提供了一种方便的方式来模拟静态方法,但它也有一些使用上的限制:
- 不能模拟
final
类:
MockedStatic
无法模拟标记为final
的类中的静态方法。例如,java.lang.System
是一个final
类,所以你不能直接使用MockedStatic
来模拟System.currentTimeMillis()
。示例代码:
// 这会抛出异常 try (MockedStatic<System> systemMock = mockStatic(System.class)) { // 这里会抛出异常,因为 System 是 final 类 }
- 不能模拟
final
方法:
如果类中的某个静态方法被标记为
final
,那么MockedStatic
也无法模拟该方法。示例代码:
public class FinalMethodExample { public static final long getSystemTime() { return System.currentTimeMillis(); } } // 这会抛出异常 try (MockedStatic<FinalMethodExample> exampleMock = mockStatic(FinalMethodExample.class)) { // 这里会抛出异常,因为 getSystemTime 是 final 方法 }
- 需要使用
mockito-inline
依赖:
要模拟静态方法,除了
mockito-core
之外,还需要添加mockito-inline
依赖。这是因为mockito-inline
提供了内联字节码操作的功能,使得能够修改静态方法的行为。示例依赖配置:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> <scope>test</scope> </dependency>
# 五、实战案例
# 1. 单元测试示例
# 1.1 基础单元测试
# 测试一个简单的Service
假设我们有一个简单的 UserService
,它依赖于 UserRepository
来获取用户信息。我们可以使用 Mockito 来模拟 UserRepository
的行为。
// UserService.java
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
// UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
}
编写测试类:
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testGetUserById() {
// Arrange
Long userId = 1L;
User user = new User(userId, "John Doe");
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
// Act
User result = userService.getUserById(userId);
// Assert
assertNotNull(result);
assertEquals("John Doe", result.getName());
}
@Test
void testGetUserById_NotFound() {
// Arrange
Long userId = 100L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// Act
User result = userService.getUserById(userId);
// Assert
assertNull(result);
}
}
# 测试DAO层
假设我们有一个 UserRepository
接口,并且我们需要测试它的实现。
// UserRepositoryImpl.java
@Repository
public class UserRepositoryImpl implements UserRepository {
@Override
public Optional<User> findById(Long id) {
// 模拟数据库查询
if (id == 1L) {
return Optional.of(new User(1L, "John Doe"));
}
return Optional.empty();
}
}
编写测试类:
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@DataJpaTest
public class UserRepositoryImplTest {
@Autowired
private UserRepository userRepository;
@Test
void testFindById_Found() {
// Act
Optional<User> result = userRepository.findById(1L);
// Assert
assertTrue(result.isPresent());
assertEquals("John Doe", result.get().getName());
}
@Test
void testFindById_NotFound() {
// Act
Optional<User> result = userRepository.findById(100L);
// Assert
assertFalse(result.isPresent());
}
}
# 1.2 模拟复杂的业务逻辑
# 测试一个具有多个依赖的服务
假设我们有一个 OrderService
,它依赖于 OrderRepository
和 PaymentService
。
// OrderService.java
public class OrderService {
private OrderRepository orderRepository;
private PaymentService paymentService;
public OrderService(OrderRepository orderRepository, PaymentService paymentService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
}
public boolean placeOrder(Order order) {
if (!paymentService.processPayment(order.getAmount())) {
return false;
}
orderRepository.save(order);
return true;
}
}
// OrderRepository.java
public interface OrderRepository extends JpaRepository<Order, Long> {
}
// PaymentService.java
public interface PaymentService {
boolean processPayment(double amount);
}
编写测试类:
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentService paymentService;
@InjectMocks
private OrderService orderService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testPlaceOrder_Success() {
// Arrange
Order order = new Order(1L, 100.0);
when(paymentService.processPayment(order.getAmount())).thenReturn(true);
// Act
boolean result = orderService.placeOrder(order);
// Assert
assertTrue(result);
verify(orderRepository).save(order);
}
@Test
void testPlaceOrder_PaymentFailed() {
// Arrange
Order order = new Order(1L, 100.0);
when(paymentService.processPayment(order.getAmount())).thenReturn(false);
// Act
boolean result = orderService.placeOrder(order);
// Assert
assertFalse(result);
verify(orderRepository, never()).save(order);
}
}
# 2. 整合Spring框架
# 2.1 在Spring Boot中使用Mockito
# 配置Spring Boot测试
在 Spring Boot 项目中,我们可以使用 @SpringBootTest
注解来配置和运行集成测试。结合 Mockito 可以方便地进行依赖注入和模拟。
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class ApplicationTests {
// 测试代码
}
# 测试Controller
假设我们有一个 UserController
,它依赖于 UserService
。
// UserController.java
@RestController
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
if (user != null) {
return ResponseEntity.ok(user);
} else {
return ResponseEntity.notFound().build();
}
}
}
编写测试类:
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void testGetUserById_Found() throws Exception {
// Arrange
Long userId = 1L;
User user = new User(userId, "John Doe");
when(userService.getUserById(userId)).thenReturn(user);
// Act & Assert
mockMvc.perform(get("/users/{id}", userId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"));
}
@Test
void testGetUserById_NotFound() throws Exception {
// Arrange
Long userId = 100L;
when(userService.getUserById(userId)).thenReturn(null);
// Act & Assert
mockMvc.perform(get("/users/{id}", userId))
.andExpect(status().isNotFound());
}
}
# 2.2 使用@MockBean
和@SpyBean
# 使用@MockBean
@MockBean
用于在 Spring 上下文中创建并注册一个 mock 对象。这使得我们在测试中可以轻松地替换掉实际的 bean。
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(UserController.class)
public class UserControllerWithMockBeanTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void testGetUserById_Found() throws Exception {
// Arrange
Long userId = 1L;
User user = new User(userId, "John Doe");
when(userService.getUserById(userId)).thenReturn(user);
// Act & Assert
mockMvc.perform(get("/users/{id}", userId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"));
}
@Test
void testGetUserById_NotFound() throws Exception {
// Arrange
Long userId = 100L;
when(userService.getUserById(userId)).thenReturn(null);
// Act & Assert
mockMvc.perform(get("/users/{id}", userId))
.andExpect(status().isNotFound());
}
}
# 使用@SpyBean
@SpyBean
用于在 Spring 上下文中创建并注册一个 spy 对象。这允许我们在测试中部分替换实际的 bean 行为。
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(UserController.class)
public class UserControllerWithSpyBeanTest {
@Autowired
private MockMvc mockMvc;
@SpyBean
private UserService userService;
@Test
void testGetUserById_Found() throws Exception {
// Arrange
Long userId = 1L;
User user = new User(userId, "John Doe");
doReturn(user).when(userService).getUserById(userId);
// Act & Assert
mockMvc.perform(get("/users/{id}", userId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"));
}
@Test
void testGetUserById_NotFound() throws Exception {
// Arrange
Long userId = 100L;
doReturn(null).when(userService).getUserById(userId);
// Act & Assert
mockMvc.perform(get("/users/{id}", userId))
.andExpect(status().isNotFound());
}
}
# 六、常见问题与解决方案
# 1. 常见错误
# 1.1 NullPointerException
NullPointerException
是在使用 Mockito 进行单元测试时常见的错误之一。这种异常通常发生在以下几种情况:
- 没有正确初始化 mock 对象。
- 在调用方法时传入了 null 参数。
示例代码:
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
错误的测试代码:
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
// 忘记初始化mock对象
}
@Test
void testGetUserById() {
Long userId = 1L;
User user = new User(userId, "John Doe");
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
User result = userService.getUserById(userId);
assertNotNull(result);
assertEquals("John Doe", result.getName());
}
}
在这个例子中,setUp
方法中忘记调用 MockitoAnnotations.openMocks(this)
,导致 userRepository
没有被初始化,从而引发 NullPointerException
。
# 1.2 NoInteractionsWanted
NoInteractionsWanted
异常通常表示在测试过程中,某个 mock 对象被意外地调用了,而我们预期该对象不应该有任何交互。
示例代码:
public class OrderService {
private PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public boolean placeOrder(Order order) {
if (order.getAmount() > 0) {
return paymentService.processPayment(order.getAmount());
}
return false;
}
}
错误的测试代码:
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class OrderServiceTest {
@Mock
private PaymentService paymentService;
@InjectMocks
private OrderService orderService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testPlaceOrder_NoPayment() {
Order order = new Order(1L, 0.0);
boolean result = orderService.placeOrder(order);
assertFalse(result);
verifyNoInteractions(paymentService); // 这里期望paymentService没有任何交互
}
}
在这个例子中,placeOrder
方法在 order.getAmount()
为 0 时不应该调用 paymentService.processPayment
,但如果实际调用了,则会抛出 NoInteractionsWanted
异常。
# 1.3 UnexpectedMethodCallException
UnexpectedMethodCallException
表示在测试过程中,某个 mock 对象被调用的方法不在预期之内。通常是因为没有正确设置 stubbing 或者调用了未定义的方法。
示例代码:
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
错误的测试代码:
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testGetUserById() {
Long userId = 1L;
User user = new User(userId, "John Doe");
// 错误的stubbing
when(userRepository.findByName("John Doe")).thenReturn(Optional.of(user));
User result = userService.getUserById(userId);
assertNotNull(result);
assertEquals("John Doe", result.getName());
}
}
在这个例子中,when(userRepository.findByName("John Doe"))
被错误地设置为返回值,而实际上应该设置 when(userRepository.findById(userId))
。这会导致 UnexpectedMethodCallException
。
# 2. 解决方案
# 2.1 确保Mock对象被初始化
在每个测试类的 setUp
方法中,确保所有 mock 对象都被正确初始化。
正确的测试代码:
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testGetUserById() {
Long userId = 1L;
User user = new User(userId, "John Doe");
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
User result = userService.getUserById(userId);
assertNotNull(result);
assertEquals("John Doe", result.getName());
}
}
# 2.2 正确设置Stubbing
确保为需要模拟的方法正确设置 stubbing,并且调用的方法和参数匹配。
正确的测试代码:
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testGetUserById() {
Long userId = 1L;
User user = new User(userId, "John Doe");
// 正确的stubbing
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
User result = userService.getUserById(userId);
assertNotNull(result);
assertEquals("John Doe", result.getName());
}
}
# 2.3 检查方法调用顺序
如果需要验证方法的调用顺序,可以使用 inOrder
来确保方法按预期顺序被调用。
正确的测试代码:
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class OrderServiceTest {
@Mock
private PaymentService paymentService;
@Mock
private OrderRepository orderRepository;
@InjectMocks
private OrderService orderService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testPlaceOrder_Success() {
Order order = new Order(1L, 100.0);
when(paymentService.processPayment(order.getAmount())).thenReturn(true);
boolean result = orderService.placeOrder(order);
assertTrue(result);
InOrder inOrder = inOrder(paymentService, orderRepository);
inOrder.verify(paymentService).processPayment(order.getAmount());
inOrder.verify(orderRepository).save(order);
}
}
# 七、总结
# 1. Mockito的核心概念回顾
在本文中,我们深入探讨了 Mockito 的核心概念和功能,包括如何创建和使用 mock 对象、验证方法调用、设置 stubbing 以及一些进阶技巧。以下是 Mockito 的一些核心概念的简要回顾:
- Mock 对象:模拟的对象,用于替代真实的依赖对象,以便进行单元测试。
- Stubbing:为 mock 对象的方法提供预定义的返回值或行为。
- Verification:验证 mock 对象的方法是否按预期被调用。
- Partial Mocking:部分模拟,允许对某些方法进行模拟,而其他方法则调用真实实现。
- Spy:监视实际对象的行为,并可以在需要时对其进行部分模拟。
- Argument Matchers:用于匹配方法参数,以便更灵活地设置 stubbing 和 verification。
- 顺序验证:验证方法调用的顺序,确保按预期顺序执行。
- 重置 Mock 对象:在多个测试方法之间重置 mock 对象的状态。
- Mock 静态方法:使用 PowerMock 等扩展库来模拟静态方法。
# 2. 最佳实践建议
为了确保您的单元测试更加可靠和易于维护,以下是一些 Mockito 使用的最佳实践建议:
- 尽可能使用接口:使用接口作为依赖注入,这样可以更容易地进行模拟。
- 最小化 Stubbing:只设置必要的 stubbing,避免过度配置。
- 明确的期望:在测试中明确说明你的期望,例如哪些方法应该被调用,哪些不应该。
- 命名清晰:为测试方法和测试类命名清晰,使其易于理解。
- 使用注解:利用
@Mock
和@InjectMocks
注解简化代码。 - 单独测试每个功能:每个测试方法应测试一个特定的功能点,保持测试的独立性。
- 验证方法调用:确保所有的方法调用都经过验证,以保证行为正确。
- 处理异常情况:在测试中处理并验证异常情况,确保代码的健壮性。
- 文档化:为复杂的测试逻辑添加注释,方便团队成员理解和维护。
祝你变得更强!