测试流程:
使用 Mockito 进行测试的一般流程可以分为以下几个步骤:
- 设置测试环境:在单元测试中,通过 Mockito 的注解或者方法来创建模拟对象(Mock)。模拟对象是用于替代真实的依赖,以便控制和测试不同的场景。
- 定义模拟行为:使用 Mockito 的方法定义模拟对象的方法行为。通常通过
when(...).thenReturn(...)
来模拟返回特定值,或者使用doThrow()
来模拟异常抛出。 - 执行测试代码:编写业务逻辑代码,将模拟对象注入依赖进行测试并断言结果。
- 验证行为:使用
verify()
验证方法调用、参数、调用次数等。
1. 准备测试环境
在测试类中准备需要的模拟对象和被测对象。可以通过 @Mock
、@InjectMocks
注解或者 Mockito.mock()
方法手动创建模拟对象。对于 Spring 项目,还可以使用 @MockBean
来替代 Spring 容器中的 Bean。关于搭建测试环境的详细说明可以看我的另一篇博客:Mockito测试环境搭建 | 整合Springboot | 常用注解详解
2、定义模拟行为:
- 使用
when(...).thenReturn(...)
来模拟方法返回值。 - 使用
doReturn(...).when(...)
来避免方法的真实调用。 - 使用
thenThrow(...)
或doThrow(...).when(...)
来模拟异常。 - 使用
thenAnswer(...)
来处理复杂的动态行为。 - 使用
doNothing()
来处理void
方法。
1. 使用 when(...).thenReturn(...)
来模拟方法返回值
这是最常用的方式。适用于模拟方法调用后需要返回某个特定值的情况。
示例:模拟 UserDao
的 getUserById()
方法在调用时返回特定的 User
对象。
@Mock
private UserDao userDao;
@Test
public void testGetUser() {
// 模拟getUserById方法,当传入用户ID为1时,返回一个新的User对象
when(userDao.getUserById(1)).thenReturn(new User(1, "John"));
// 调用测试方法
User user = userDao.getUserById(1);
// 断言结果
assertEquals("John", user.getName());
}
when(...).thenReturn(...)
:表示当调用 userDao.getUserById(1)
时,返回 new User(1, "John")
。
2. 使用 doReturn(...).when(...)
来模拟方法返回值
与 when(...).thenReturn(...)
类似,但适用于某些特殊情况,例如需要避免实际调用真实方法(尤其是部分模拟 @Spy
时),或者处理 void
方法的情况。
示例:使用 doReturn
避免部分模拟的真实方法被调用。
@Spy
private UserDao userDao;
@Test
public void testSaveUser() {
// 避免调用真实的saveUser方法
doReturn(1).when(userDao).saveUser(any(User.class));
// 调用saveUser方法
userDao.saveUser(new User(2, "Doe"));
// 验证saveUser确实被调用过一次
verify(userDao, times(1)).saveUser(any(User.class));
}
doReturn(...).when(...)
:适用于你不希望真实调用某个方法的情况。
when.thenReturn和doReturn.when的区别
1. when(...).thenReturn(...)
这是 Mockito 的标准使用方式,用于定义当某个方法被调用时返回指定的值。它适用于绝大多数场景,尤其是当你使用完全模拟对象(即 @Mock
)时。
这种写法会去实际执行代码,然后返回指定值
示例:
when(userDao.getUserById(2)).thenReturn(null);
- 工作原理:Mockito 在内部是通过调用
userDao.getUserById(2)
方法,并在该方法执行后记录这个调用,然后当方法被再次调用时,返回null
。 - 调用时机:
when(...).thenReturn(...)
实际上会首先调用目标方法getUserById(2)
,然后再返回指定的结果。如果目标方法是有副作用的(比如修改某些状态),它会先执行副作用,再进行模拟。
2. doReturn(...).when(...)
doReturn(...).when(...)
是 Mockito 中的另一种方法,主要用于避免方法调用本身带来的副作用,尤其是在 部分模拟(@Spy
)的场景中非常有用。
这种写法不会执行代码,直接返回指定值。
示例:
doReturn(null).when(userDao).getUserById(2);
- 工作原理:
doReturn(null)
先定义了模拟的返回值,然后使用when(userDao)
来指定在getUserById(2)
方法被调用时返回null
,而不会先调用getUserById(2)
方法的真实实现。 - 调用时机:
doReturn(...).when(...)
不会实际调用目标方法,因此不会触发任何真实方法的执行。如果目标方法有副作用或复杂的逻辑,使用doReturn(...)
可以避免这些问题。
什么时候使用 doReturn(...).when(...)
?
-
处理
void
方法:when(...).thenReturn(...)
不能用于模拟void
方法,因为void
方法没有返回值。这时你需要使用doReturn()
或doThrow()
来模拟void
方法的行为。示例:
doNothing().when(mockObject).someVoidMethod();
-
部分模拟(
@Spy
)的场景:当你使用部分模拟(@Spy
)时,when(...).thenReturn(...)
实际上会调用真实方法。如果你不希望调用真实方法(比如该方法会改变对象状态或有副作用),可以使用doReturn(...)
来避免真实方法的调用。示例:
@Spy private UserDao userDao; // 避免真实调用 doReturn(null).when(userDao).getUserById(2);
在这种情况下,
when(userDao.getUserById(2)).thenReturn(null)
会实际调用getUserById(2)
,但使用doReturn(null)
则不会调用真实方法。 -
方法抛出异常的场景:某些情况下,方法在实际调用时会抛出异常。如果你不希望方法抛出异常(例如,你只关心返回结果的模拟),使用
doReturn(...)
可以避免直接调用导致的异常。示例:
doReturn(null).when(userDao).getUserById(2);
如果
userDao.getUserById(2)
的真实方法抛出了异常,而你希望避免这种情况,则使用doReturn(...)
可以跳过真实方法调用。
when(...).thenReturn(...)
的局限性
会实际调用方法:如果目标方法会触发某些副作用(例如修改数据或引发异常),when(...).thenReturn(...)
会首先调用该方法,然后记录返回结果,这有时不是你想要的行为,特别是在 @Spy
场景中。
示例:当使用部分模拟(@Spy
)时,以下代码会先调用 getUserById(2)
,即真实方法会被调用:
when(userDao.getUserById(2)).thenReturn(null);
不能用于 void
方法:因为 when(...).thenReturn(...)
是针对有返回值的方法,如果你想模拟 void
方法(即不返回值的方法),则需要使用 doReturn()
、doThrow()
等方法。
3. 模拟方法抛出异常
可以使用 thenThrow(...)
或 doThrow(...).when(...)
来模拟方法在被调用时抛出异常的场景,适合用于测试异常处理逻辑。
示例:模拟 saveUser
方法在调用时抛出异常。
@Mock
private UserDao userDao;
@Test
public void testSaveUserThrowsException() {
// 模拟saveUser方法抛出异常
doThrow(new RuntimeException("Database error")).when(userDao).saveUser(any(User.class));
// 捕获异常
assertThrows(RuntimeException.class, () -> {
userDao.saveUser(new User(1, "John"));
});
// 验证saveUser方法确实被调用过一次
verify(userDao, times(1)).saveUser(any(User.class));
}
doThrow(...).when(...)
或 thenThrow(...)
:模拟方法抛出异常,用于测试异常处理逻辑。
4. 模拟 void
方法的行为
doNothing()
和 doThrow()
是最常用的处理 void
方法的方式,前者模拟不执行任何操作,后者模拟抛出异常。
示例:模拟 void
方法 deleteUser
执行时不做任何事情。
@Mock
private UserDao userDao;
@Test
public void testDeleteUser() {
// 模拟deleteUser方法执行时什么都不做
doNothing().when(userDao).deleteUser(anyInt());
// 调用测试方法
userDao.deleteUser(1);
// 验证deleteUser确实被调用过
verify(userDao, times(1)).deleteUser(1);
}
doNothing().when(...)
:用于模拟 void
方法的行为,表示该方法什么都不做。
5. 使用 thenAnswer(...)
来模拟复杂行为
thenAnswer()
允许你根据传入的参数、方法的调用上下文、甚至外部状态来动态地生成返回值或执行特定逻辑。相比于 thenReturn()
这种简单的返回值模拟方式,thenAnswer()
提供了更大的灵活性。
thenAnswer()
的主要特点:
- 基于输入参数动态响应:你可以根据方法的输入参数来生成不同的返回结果。
- 执行自定义逻辑:它允许你在模拟方法中执行特定的自定义逻辑,而不仅仅是返回一个固定值。
- 复杂行为模拟:适用于更复杂的业务场景,比如多个条件组合下的不同返回值,或者需要根据传入参数执行计算等。
thenAnswer()
使用方法
thenAnswer()
接受一个 Answer
接口的实现作为参数。Answer
接口定义了一个 answer(InvocationOnMock invocation)
方法,该方法会在模拟方法被调用时执行。你可以通过这个方法来访问方法的调用信息(包括传入的参数),并根据需要自定义返回结果或逻辑。
//Answer 接口定义
public interface Answer<T> {
T answer(InvocationOnMock invocation) throws Throwable;
}
InvocationOnMock
:提供了对当前调用的所有信息,包括参数、调用的 mock 对象等。answer()
:在方法被调用时触发,用于自定义返回值或执行逻辑。
InvocationOnMock
接口提供了几个常用的方法,允许你访问模拟方法调用的详细信息:
getMock()
:返回当前被调用的模拟对象。getMethod()
:返回被调用的Method
对象。getArguments()
:返回方法的所有传递参数的数组。getArgument(int index)
:返回指定索引位置的单个参数。getArgument(int index, Class<T> clazz)
:返回指定索引的参数并强制转换为指定类型。getArgumentsCount()
:返回传递的参数数量。callRealMethod()
:调用被模拟方法的真实实现(常用于部分模拟@Spy
)。
示例:根据传入的参数动态返回不同结果
假设有一个 UserDao
的 getUserById
方法,你希望根据传入的用户 ID 来动态地返回不同的用户对象。
@Mock
private UserDao userDao;
@Test
public void testGetUserWithAnswer() {
// 使用thenAnswer根据传入的参数返回不同的结果
when(userDao.getUserById(anyInt())).thenAnswer(new Answer<User>() {
@Override
public User answer(InvocationOnMock invocation) throws Throwable {
// 获取传入的参数(即用户ID)
int userId = invocation.getArgument(0);
// 根据用户ID返回不同的User对象
return new User(userId, "User" + userId);
}
});
// 调用测试方法
User user1 = userDao.getUserById(1);
User user2 = userDao.getUserById(2);
// 验证返回结果
assertEquals("User1", user1.getName());
assertEquals("User2", user2.getName());
}
解释:
thenAnswer(new Answer<User>() {...})
:在getUserById
方法被调用时,触发Answer
接口的answer()
方法来生成返回值。invocation.getArgument(0)
:获取方法调用时的第一个参数,这里是传入的userId
。- 动态生成返回结果:根据传入的
userId
,返回不同的User
对象。
示例:抛出异常的场景
假设你想根据方法的传入参数决定是否抛出异常,可以使用 thenAnswer()
来实现。
@Mock
private UserDao userDao;
@Test
public void testGetUserThrowsException() {
// 使用thenAnswer来模拟不同的异常抛出条件
when(userDao.getUserById(anyInt())).thenAnswer(new Answer<User>() {
@Override
public User answer(InvocationOnMock invocation) throws Throwable {
int userId = invocation.getArgument(0);
if (userId < 0) {
throw new IllegalArgumentException("User ID cannot be negative");
}
return new User(userId, "User" + userId);
}
});
// 验证抛出异常
assertThrows(IllegalArgumentException.class, () -> {
userDao.getUserById(-1);
});
// 正常调用不抛异常
User user = userDao.getUserById(1);
assertEquals("User1", user.getName());
}
解释:
- 根据传入参数抛出异常:当传入的
userId
小于 0 时,抛出IllegalArgumentException
,否则正常返回用户对象。 - 测试不同情况:我们通过
assertThrows
来验证方法在传入非法参数时抛出了预期的异常。
复杂场景:模拟调用次数或外部状态的变化
有时,你可能需要根据方法被调用的次数或外部状态的变化来决定返回值或执行逻辑。thenAnswer()
可以处理这些复杂场景。
示例:根据调用次数返回不同的结果
@Mock
private UserDao userDao;
@Test
public void testGetUserWithMultipleCalls() {
// 使用thenAnswer来根据调用次数返回不同的结果
when(userDao.getUserById(anyInt())).thenAnswer(new Answer<User>() {
private int callCount = 0; // 记录调用次数
@Override
public User answer(InvocationOnMock invocation) throws Throwable {
callCount++;
if (callCount == 1) {
return new User(1, "First Call User");
} else {
return new User(1, "Subsequent Call User");
}
}
});
// 第一次调用返回"First Call User"
User user1 = userDao.getUserById(1);
assertEquals("First Call User", user1.getName());
// 第二次及之后的调用返回"Subsequent Call User"
User user2 = userDao.getUserById(1);
assertEquals("Subsequent Call User", user2.getName());
}
解释:
callCount
:通过一个计数器callCount
来记录方法的调用次数。- 根据调用次数返回不同的结果:第一次调用返回一个结果,后续调用返回不同的结果。
thenAnswer()
的优势
- 灵活性高:
thenAnswer()
允许你在方法被调用时基于参数或其他条件生成返回值或抛出异常,比简单的thenReturn()
更加灵活。 - 动态行为模拟:可以根据方法的参数、调用次数、甚至外部状态来决定模拟的行为,适用于复杂的测试场景。
- 测试特定逻辑:它可以帮助你测试依赖复杂逻辑的代码片段,尤其是在简单的
thenReturn()
或thenThrow()
无法满足需求时。
3. 执行测试代码并断言
编写测试代码,调用被测类中的方法。由于被测类的依赖已经被模拟对象替换,所以你可以专注于测试当前方法的逻辑,而不必担心真实依赖带来的副作用。在执行完测试代码后,你可以通过 JUnit 的断言 来检查测试结果是否符合预期。常用的断言包括 assertEquals()
、assertTrue()
、assertNull()
等。关于Junit常用的断言方法也可以参照我的另一篇博客:Junit常用断言方法超全详解 | 新手入门JUnit
4、验证行为:
验证行为的常用方法
verify()
:验证某个模拟对象的方法是否被调用。verifyNoMoreInteractions()
:验证某个模拟对象的方法在指定的调用之外,没有其他额外的调用。verifyZeroInteractions()
/verifyNoInteractions()
:验证某个模拟对象从未被调用。InOrder
:验证多个方法调用的顺序。times()
:验证某个方法被调用的次数。never()
:验证某个方法从未被调用。atLeast()
和atMost()
:验证某个方法至少/至多被调用多少次。
1. 使用 verify()
验证方法调用
verify()
是 Mockito 验证调用关系的核心方法。它用于验证某个模拟对象的特定方法是否被调用。
示例:验证方法被调用
@Mock
private UserDao userDao;
@Test
public void testVerifyMethodCall() {
// 模拟getUserById方法的行为
when(userDao.getUserById(1)).thenReturn(new User(1, "John"));
// 调用被测方法
User user = userDao.getUserById(1);
// 验证getUserById方法是否被调用过
verify(userDao).getUserById(1);
}
在这个例子中,verify(userDao).getUserById(1)
验证了 getUserById(1)
是否被调用了一次。
2. 使用 times()
验证调用次数
有时,你不仅要验证方法是否被调用,还要验证它被调用了几次。Mockito 提供了 times()
方法来验证调用的次数。
示例:验证方法被调用的次数
@Mock
private UserDao userDao;
@Test
public void testVerifyCallTimes() {
// 模拟调用行为
when(userDao.getUserById(1)).thenReturn(new User(1, "John"));
// 调用多次方法
userDao.getUserById(1);
userDao.getUserById(1);
// 验证getUserById方法被调用了2次
verify(userDao, times(2)).getUserById(1);
}
times(2)
:验证 getUserById(1)
被调用了两次。
3. 使用 never()
验证方法从未被调用
如果你需要验证某个方法从未被调用过,可以使用 never()
。
示例:验证方法从未被调用
@Mock
private UserDao userDao;
@Test
public void testVerifyNeverCalled() {
// 调用其他方法
userDao.saveUser(new User(1, "John"));
// 验证getUserById方法从未被调用
verify(userDao, never()).getUserById(1);
}
never()
:验证 getUserById(1)
从未被调用过。
4. 使用 verifyNoMoreInteractions()
验证没有其他方法调用
verifyNoMoreInteractions()
用于确保在验证指定的方法调用之后,没有其他不必要的调用。
示例:验证没有额外的调用
@Mock
private UserDao userDao;
@Test
public void testVerifyNoMoreInteractions() {
// 调用一个方法
userDao.getUserById(1);
// 验证getUserById方法被调用过
verify(userDao).getUserById(1);
// 验证没有其他多余的方法调用
verifyNoMoreInteractions(userDao);
}
verifyNoMoreInteractions()
:确保除了 getUserById(1)
外,userDao
没有其他方法被调用。
5. 使用 verifyZeroInteractions()
或 verifyNoInteractions()
验证没有任何交互
verifyZeroInteractions()
(或 verifyNoInteractions()
)用于验证某个模拟对象的任何方法都没有被调用过。
示例:验证没有任何方法调用
@Mock
private UserDao userDao;
@Test
public void testVerifyZeroInteractions() {
// 不调用任何方法
// 验证userDao没有任何方法被调用
verifyZeroInteractions(userDao);
// 或者使用 verifyNoInteractions(userDao);
}
verifyZeroInteractions()
或 verifyNoInteractions()
:验证 userDao
没有任何方法被调用。
6. 使用 InOrder
验证调用顺序
InOrder
用于验证方法的调用顺序。如果你有多个方法调用,并且需要确保它们按特定顺序调用,InOrder
非常有用。
示例:验证方法调用的顺序
@Mock
private UserDao userDao;
@Mock
private NotificationService notificationService;
@Test
public void testVerifyCallOrder() {
// 调用多个方法
userDao.saveUser(new User(1, "John"));
notificationService.notifyUser(1);
// 验证调用顺序
InOrder inOrder = inOrder(userDao, notificationService);
inOrder.verify(userDao).saveUser(any(User.class)); // 验证saveUser先被调用
inOrder.verify(notificationService).notifyUser(1); // 然后notifyUser被调用
}
inOrder()
:验证 saveUser
和 notifyUser
方法是否按照指定的顺序被调用。
7. 使用 atLeast()
和 atMost()
验证调用的最小/最大次数
如果你需要验证某个方法被调用的次数在某个范围内,Mockito 提供了 atLeast()
和 atMost()
来验证方法的最少和最多调用次数。
示例:验证调用的最小次数
@Mock
private UserDao userDao;
@Test
public void testVerifyAtLeast() {
// 调用方法多次
userDao.getUserById(1);
userDao.getUserById(1);
userDao.getUserById(1);
// 验证getUserById方法至少被调用2次
verify(userDao, atLeast(2)).getUserById(1);
}
atLeast(2)
:验证 getUserById(1)
至少被调用了 2 次。
示例:验证调用的最大次数
@Mock
private UserDao userDao;
@Test
public void testVerifyAtMost() {
// 调用方法多次
userDao.getUserById(1);
userDao.getUserById(1);
// 验证getUserById方法最多被调用2次
verify(userDao, atMost(2)).getUserById(1);
}
atMost(2)
:验证 getUserById(1)
最多被调用了 2 次。
具体演示:
class UserServiceTest {
//1、准备测试环境
@Mock
WebServiceClient webServiceClient;
@Mock
UserDao userDao;
@InjectMocks
UserService userService;
//初始化Mock
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
//测试用例,从本地获取用户数据
@Test
void getLocalDBUser() {
User MockUser = User.builder().id(1).name("张三").build();
//2、定义模拟行为
when(userDao.getUserById(1)).thenReturn(MockUser);
//3、业务测试代码
User ReturnedUser = userService.getUser(1);
//断言结果
assertEquals("张三",ReturnedUser.getName());
//4、验证行为
verify(userDao,times(1)).getUserById(1);
}
}