1. 简介
1.1. 概述
这篇文章将讨论 Mock 测试 的基本概念、使用原因,并通过一个统一的测试场景,分别使用 Mockito、EasyMock 和 JMockit 这三个主流 Java Mock 框架进行实现,从而帮助你更直观地比较它们的用法和优劣。
我们会从 Mock 的基本概念入手,接着定义一个通用的测试用例,然后分别用三个框架实现,最后给出对比结论。如果你已经对 Mock 有所了解,可以直接跳到第 2 节。
1.2. 为什么使用 Mock
假设你已经在使用 TDD、ATDD 或 BDD 等以测试为中心的开发方式,或者你正在为一个已有类编写测试。在对某个类进行单元测试时,我们只关心它本身的行为,而不是它依赖的其他组件。
为了实现这一点,我们需要用 可控的替代对象 来替换真实依赖。这样可以强制返回特定值、抛出异常,或者跳过耗时操作。这种替代对象就是 Mock 对象,它可以帮助我们简化测试代码并提升执行效率。
1.3. Mock 相关术语定义
Martin Fowler 的 这篇文章 很好地总结了 Mock 的基本概念:
- Dummy:用于填充参数列表的对象,但不会被实际使用。
- Fake:拥有实际功能实现的对象,但通常走的是捷径,不适合生产环境(如内存数据库)。
- Stub:为测试提供预设返回值的对象,通常不处理预设之外的调用。也可以记录调用信息。
- Mock:我们这里讨论的对象,预先设定好期望行为,用于验证是否被正确调用。
1.4. 是否需要 Mock:这是个问题
并不是所有对象都需要 Mock。有时候直接做集成测试比 Mock 更有价值。比如我们要测试的 LoginDao
,它依赖第三方数据库库。Mock 它只能验证参数是否准备正确,但无法验证实际数据库返回是否符合预期。
因此,本文将不对 LoginDao
进行 Mock,而是直接使用真实调用(虽然也可以同时写单元测试和集成测试)。
2. 测试用例
2.1. 用例说明
我们将使用一个典型的分层架构登录流程作为测试用例:
- Controller 接收登录请求
- Service 层处理业务逻辑
- DAO 层访问数据库验证用户信息
我们将重点测试各组件之间的交互,而不是具体实现。
结构图如下:
2.2. 实现代码
用户表单类 UserForm
public class UserForm {
public String password;
public String username;
public String getUsername() {
return username;
}
}
数据访问类 LoginDao
public class LoginDao {
public int login(UserForm userForm) {
return 0;
}
}
服务类 LoginService
public class LoginService {
private LoginDao loginDao;
private String currentUser;
public boolean login(UserForm userForm) {
assert null != userForm;
int loginResults = loginDao.login(userForm);
switch (loginResults) {
case 1:
return true;
default:
return false;
}
}
public void setCurrentUser(String username) {
if (null != username) {
this.currentUser = username;
}
}
}
控制器类 LoginController
public class LoginController {
public LoginService loginService;
public String login(UserForm userForm) {
if (null == userForm) {
return "ERROR";
} else {
boolean logged;
try {
logged = loginService.login(userForm);
} catch (Exception e) {
return "ERROR";
}
if (logged) {
loginService.setCurrentUser(userForm.getUsername());
return "OK";
} else {
return "KO";
}
}
}
}
3. 测试环境搭建
3.1. Mockito
使用版本:2.8.9
常用注解:
@Mock
:创建 Mock 对象@InjectMocks
:注入 Mock 对象到被测试对象@Spy
:创建部分 Mock(保留真实实现)
public class LoginControllerTest {
@Mock
private LoginDao loginDao;
@Spy
@InjectMocks
private LoginService spiedLoginService;
@Mock
private LoginService loginService;
@InjectMocks
private LoginController loginController;
@Before
public void setUp() {
loginController = new LoginController();
MockitoAnnotations.initMocks(this);
}
}
3.2. EasyMock
使用版本:3.4
使用 @RunWith(EasyMockRunner.class)
简化测试:
@RunWith(EasyMockRunner.class)
public class LoginControllerTest {
@Mock
private LoginDao loginDao;
@Mock
private LoginService loginService;
@TestSubject
private LoginController loginController = new LoginController();
}
3.3. JMockit
使用版本:1.49
使用注解:
@Injectable
:创建单个 Mock 实例@Mocked
:Mock 所有实例@Tested
:创建被测试对象并自动注入 Mock
public class LoginControllerIntegrationTest {
@Injectable
private LoginDao loginDao;
@Injectable
private LoginService loginService;
@Tested
private LoginController loginController;
}
4. 验证 Mock 未被调用
4.1. Mockito
@Test
public void assertThatNoMethodHasBeenCalled() {
loginController.login(null);
Mockito.verifyNoInteractions(loginService);
}
4.2. EasyMock
@Test
public void assertThatNoMethodHasBeenCalled() {
EasyMock.replay(loginService);
loginController.login(null);
EasyMock.verify(loginService);
}
4.3. JMockit
@Test
public void assertThatNoMethodHasBeenCalled() {
loginController.login(null);
new FullVerifications(loginService) {};
}
5. 定义 Mock 行为并验证调用
5.1. Mockito
@Test
public void assertTwoMethodsHaveBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
Mockito.when(loginService.login(userForm)).thenReturn(true);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
Mockito.verify(loginService).login(userForm);
Mockito.verify(loginService).setCurrentUser("foo");
}
@Test
public void assertOnlyOneMethodHasBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
Mockito.when(loginService.login(userForm)).thenReturn(false);
String login = loginController.login(userForm);
Assert.assertEquals("KO", login);
Mockito.verify(loginService).login(userForm);
Mockito.verifyNoMoreInteractions(loginService);
}
5.2. EasyMock
@Test
public void assertTwoMethodsHaveBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
EasyMock.expect(loginService.login(userForm)).andReturn(true);
loginService.setCurrentUser("foo");
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
EasyMock.verify(loginService);
}
@Test
public void assertOnlyOneMethodHasBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
EasyMock.expect(loginService.login(userForm)).andReturn(false);
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("KO", login);
EasyMock.verify(loginService);
}
5.3. JMockit
@Test
public void assertTwoMethodsHaveBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
new Expectations() {{
loginService.login(userForm); result = true;
loginService.setCurrentUser("foo");
}};
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
new FullVerifications(loginService) {};
}
@Test
public void assertOnlyOneMethodHasBeenCalled() {
UserForm userForm = new UserForm();
userForm.username = "foo";
new Expectations() {{
loginService.login(userForm); result = false;
// no expectation for setCurrentUser
}};
String login = loginController.login(userForm);
Assert.assertEquals("KO", login);
new FullVerifications(loginService) {};
}
6. Mock 抛出异常
6.1. Mockito
@Test
public void mockExceptionThrowing() {
UserForm userForm = new UserForm();
Mockito.when(loginService.login(userForm)).thenThrow(IllegalArgumentException.class);
String login = loginController.login(userForm);
Assert.assertEquals("ERROR", login);
Mockito.verify(loginService).login(userForm);
Mockito.verifyNoInteractions(loginService);
}
6.2. EasyMock
@Test
public void mockExceptionThrowing() {
UserForm userForm = new UserForm();
EasyMock.expect(loginService.login(userForm)).andThrow(new IllegalArgumentException());
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("ERROR", login);
EasyMock.verify(loginService);
}
6.3. JMockit
@Test
public void mockExceptionThrowing() {
UserForm userForm = new UserForm();
new Expectations() {{
loginService.login(userForm); result = new IllegalArgumentException();
// no expectation for setCurrentUser
}};
String login = loginController.login(userForm);
Assert.assertEquals("ERROR", login);
new FullVerifications(loginService) {};
}
7. Mock 传递对象
7.1. Mockito
@Test
public void mockAnObjectToPassAround() {
UserForm userForm = Mockito.when(Mockito.mock(UserForm.class).getUsername())
.thenReturn("foo").getMock();
Mockito.when(loginService.login(userForm)).thenReturn(true);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
Mockito.verify(loginService).login(userForm);
Mockito.verify(loginService).setCurrentUser("foo");
}
7.2. EasyMock
@Test
public void mockAnObjectToPassAround() {
UserForm userForm = EasyMock.mock(UserForm.class);
EasyMock.expect(userForm.getUsername()).andReturn("foo");
EasyMock.expect(loginService.login(userForm)).andReturn(true);
loginService.setCurrentUser("foo");
EasyMock.replay(userForm);
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
EasyMock.verify(userForm);
EasyMock.verify(loginService);
}
7.3. JMockit
@Test
public void mockAnObjectToPassAround(@Mocked UserForm userForm) {
new Expectations() {{
userForm.getUsername(); result = "foo";
loginService.login(userForm); result = true;
loginService.setCurrentUser("foo");
}};
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
new FullVerifications(loginService) {};
new FullVerifications(userForm) {};
}
8. 自定义参数匹配
8.1. Mockito
@Test
public void argumentMatching() {
UserForm userForm = new UserForm();
userForm.username = "foo";
// default matcher
Mockito.when(loginService.login(Mockito.any(UserForm.class))).thenReturn(true);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
Mockito.verify(loginService).login(userForm);
// complex matcher
Mockito.verify(loginService).setCurrentUser(ArgumentMatchers.argThat(
new ArgumentMatcher<String>() {
@Override
public boolean matches(String argument) {
return argument.startsWith("foo");
}
}
));
}
8.2. EasyMock
@Test
public void argumentMatching() {
UserForm userForm = new UserForm();
userForm.username = "foo";
// default matcher
EasyMock.expect(loginService.login(EasyMock.isA(UserForm.class))).andReturn(true);
// complex matcher
loginService.setCurrentUser(specificArgumentMatching("foo"));
EasyMock.replay(loginService);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
EasyMock.verify(loginService);
}
private static String specificArgumentMatching(String expected) {
EasyMock.reportMatcher(new IArgumentMatcher() {
@Override
public boolean matches(Object argument) {
return argument instanceof String
&& ((String) argument).startsWith(expected);
}
@Override
public void appendTo(StringBuffer buffer) {
//NOOP
}
});
return null;
}
8.3. JMockit
@Test
public void argumentMatching() {
UserForm userForm = new UserForm();
userForm.username = "foo";
// default matcher
new Expectations() {{
loginService.login((UserForm) any);
result = true;
// complex matcher
loginService.setCurrentUser(with(new Delegate<String>() {
@Override
public boolean matches(Object item) {
return item instanceof String && ((String) item).startsWith("foo");
}
}));
}};
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
new FullVerifications(loginService) {};
}
9. 部分 Mock(Partial Mocking)
9.1. Mockito
@Test
public void partialMocking() {
// use partial mock
loginController.loginService = spiedLoginService;
UserForm userForm = new UserForm();
userForm.username = "foo";
// let service's login use implementation so let's mock DAO call
Mockito.when(loginDao.login(userForm)).thenReturn(1);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
// verify mocked call
Mockito.verify(spiedLoginService).setCurrentUser("foo");
}
9.2. EasyMock
@Test
public void partialMocking() {
UserForm userForm = new UserForm();
userForm.username = "foo";
// use partial mock
LoginService loginServicePartial = EasyMock.partialMockBuilder(LoginService.class)
.addMockedMethod("setCurrentUser").createMock();
loginServicePartial.setCurrentUser("foo");
// let service's login use implementation so let's mock DAO call
EasyMock.expect(loginDao.login(userForm)).andReturn(1);
loginServicePartial.setLoginDao(loginDao);
loginController.loginService = loginServicePartial;
EasyMock.replay(loginDao);
EasyMock.replay(loginServicePartial);
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
// verify mocked call
EasyMock.verify(loginServicePartial);
EasyMock.verify(loginDao);
}
9.3. JMockit
@Test
public void partialMocking() {
LoginService partialLoginService = new LoginService();
partialLoginService.setLoginDao(loginDao);
loginController.loginService = partialLoginService;
UserForm userForm = new UserForm();
userForm.username = "foo";
new Expectations(partialLoginService) {{
// let's mock DAO call
loginDao.login(userForm); result = 1;
// no expectation for login method so that real implementation is used
// mock setCurrentUser call
partialLoginService.setCurrentUser("foo");
}};
String login = loginController.login(userForm);
Assert.assertEquals("OK", login);
// verify mocked call
new Verifications() {{
partialLoginService.setCurrentUser("foo");
}};
}
10. 总结
在这篇文章中,我们比较了三种主流的 Java Mock 框架:
✅ 配置易用性:三者都支持注解,但 Mockito 和 JMockit 更简洁
✅ 结构清晰度:JMockit 强制使用 Expectations
和 Verifications
块,结构最清晰
✅ 语法一致性:JMockit 的 result = ...
语法适用于正常返回和异常抛出
✅ 社区支持:Mockito 仍是最多人使用的,生态更成熟
❌ EasyMock 需要手动 replay:这是个明显的缺点
✅ 部分 Mock:三者都支持,JMockit 实现最自然
综合来看,我们推荐 JMockit 作为首选 Mock 框架,尽管我们过去常用 Mockito。它的结构清晰、语法一致,非常适合构建高质量的测试代码。
完整代码可在 GitHub 获取。