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. 用例说明

我们将使用一个典型的分层架构登录流程作为测试用例:

  1. Controller 接收登录请求
  2. Service 层处理业务逻辑
  3. DAO 层访问数据库验证用户信息

我们将重点测试各组件之间的交互,而不是具体实现。

结构图如下:

Test case diagram

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 强制使用 ExpectationsVerifications 块,结构最清晰
语法一致性:JMockit 的 result = ... 语法适用于正常返回和异常抛出
社区支持:Mockito 仍是最多人使用的,生态更成熟
EasyMock 需要手动 replay:这是个明显的缺点
部分 Mock:三者都支持,JMockit 实现最自然

综合来看,我们推荐 JMockit 作为首选 Mock 框架,尽管我们过去常用 Mockito。它的结构清晰、语法一致,非常适合构建高质量的测试代码。

完整代码可在 GitHub 获取。


原始标题:Mockito vs EasyMock vs JMockit