为什么 Mockito 测试模拟而不是被测对象?

Why does Mockito test the mock instead of the object under test?

我刚刚开始熟悉 Mockito,我发现它不是特别有用。

我有一个 View 和一个 Presenter。视图是一个哑巴 activity,而演示者包含所有业务逻辑。我想模拟 View 并测试 Presenter 的工作方式。

Mockito 来了,我可以成功模拟 View,这两个单元测试工作正常:

@Test
public void testWhenUserNameIsEmptyShowErrorOnLoginClicked() throws Exception {
    Mockito.when(loginView.getUserName()).thenReturn("");
    Mockito.when(loginView.getPassword()).thenReturn("asdasd");
    loginPresenter.setLoginView(loginView);
    loginPresenter.onLoginClicked();
    Mockito.verify(loginView).setEmailFieldErrorMessage();
}

@Test
public void testWhenPasswordIsEmptyShowErrorOnPasswordClicked() throws Exception {
    Mockito.when(loginView.getUserName()).thenReturn("George");
    Mockito.when(loginView.getPassword()).thenReturn("");
    loginPresenter.setLoginView(loginView);
    loginPresenter.onLoginClicked();
    Mockito.verify(loginView).setPasswordFieldErrorMessage();
}

但是,如果我想测试演示者的内部方法,这是行不通的:

@Test
public void testWhenUserNameAndPasswordAreEnteredShouldAttemptLogin() throws Exception {
    LoginView loginView = Mockito.mock(LoginView.class);
    Mockito.when(loginView.getUserName()).thenReturn("George");
    Mockito.when(loginView.getPassword()).thenReturn("aaaaaa");
    loginPresenter.setLoginView(loginView);
    loginPresenter.onLoginClicked();
    Mockito.verify(loginPresenter).attemptLogin(loginView.getUserName(), loginView.getPassword());
}

它抛出一个 NotAMockException - 它说这个对象应该是一个 Mock。我为什么要测试模拟?这是测试中的首要规则之一 - 你不会创建一个模拟然后测试它,你有一个你想要测试的对象,如果它需要一些依赖 - 你模拟它们。

也许我没有正确理解 Mockito,但这样对我来说似乎没用。我该怎么办?

理想情况下,Mockito 应该只用于模拟和验证外部服务。您确定您拥有它的方式是次优的是正确的,主要是因为您测试您的实施而不是您的总合同

// Doesn't work unless loginPresenter is a mock.
verify(loginPresenter)
    .attemptLogin(loginView.getUserName(), loginView.getPassword());

从技术角度来看,Mockito 只能对模拟方法进行存根和验证。这是因为,在表面之下,Mockito 无法深入并检查系统中每个 class 之间的每个交互;它的 "mocks" 是 subclasses 或 proxies,它们有效地覆盖了每一个方法来记录交互以进行验证并以您存根的方式响应。这意味着如果你想调用 whenverify 因为它适用于你的演示者,它需要在模拟或间谍对象上的非最终非静态方法上,你是正确的观察是,这会很容易无意中测试 Mockito 是否正常工作,而不是测试您的单元或系统是否正常工作。

在您的情况下,您似乎将被测单元视为单一onLoginClicked方法,其中包括存根和验证其交互在您的演示者上使用其他方法。这称为 "partial mocking" 并且在某些情况下实际上是一种有效的测试策略,特别是当您大量测试轻量级方法并且该轻量级方法在同一对象上调用更重的方法时。虽然您通常可以通过重构(以及通过设计可测试组件)避免部分模拟,但它仍然是工具箱中的一个有用工具。

// Partial mocking example
@Test
public void testWhenUserNameAndPasswordAreEnteredShouldAttemptLogin() {
  LoginView loginView = Mockito.mock(LoginView.class);

  // Use a spy, which delegates to the original object by default.
  loginPresenter = Mockito.spy(new LoginPresenter());

  Mockito.when(loginView.getUserName()).thenReturn("George");
  Mockito.when(loginView.getPassword()).thenReturn("aaaaaa");
  loginPresenter.setLoginView(loginView);
  loginPresenter.onLoginClicked();
  // Beware! You can get weird errors if calling a method on a mock in the
  // middle of stubbing or verification.
  Mockito.verify(loginPresenter)
      .attemptLogin(loginView.getUserName(), loginView.getPassword());
}

当然,不用 Mockito 也可以这样做:

String capturedUsername;
String capturedPassword;

public void testWhenUserNameAndPasswordAreEnteredShouldAttemptLogin_noMockito() {
  // Same as above, with an anonymous inner class instead of Mockito.
  LoginView loginView = Mockito.mock(LoginView.class);
  loginPresenter = new LoginPresenter() {

    @Override public void attemptLogin(String username, String password) {
      capturedUsername = username;
      capturedPassword = password;
    }
  };

  Mockito.when(loginView.getUserName()).thenReturn("George");
  Mockito.when(loginView.getPassword()).thenReturn("aaaaaa");
  loginPresenter.setLoginView(loginView);
  loginPresenter.onLoginClicked();
  assertEquals("George", capturedUsername);
  assertEquals("aaaaaa", capturedPassword);
}

尽管如此,更有价值的策略可能是 将整个 Presenter 视为被测单元 并且 仅测试您的主持人的外部互动。此时,对 attemptLogin 的调用应该是您的测试不关心的实现细节,这样您就可以随心所欲地重构它。

attemptLogin 在外部运行时会发生什么?也许这里的外部交互是您的 Presenter 使用正确的参数启动到路径 LoginEndpoint.Login 的 RPC。然后,而不是 verify 在你的演示者中实现细节,你 verify 它与外界的交互——这正是 Mockito 设计的目的。

@Test
public void testWhenUserNameAndPasswordAreEnteredShouldAttemptLogin() {
  LoginView loginView = Mockito.mock(LoginView.class);
  RpcService rpcService = Mockito.mock(RpcService.class);
  Mockito.when(loginView.getUserName()).thenReturn("George");
  Mockito.when(loginView.getPassword()).thenReturn("aaaaaa");
  loginPresenter.setLoginView(loginView);
  loginPresenter.setRpcService(rpcService);
  loginPresenter.onLoginClicked();
  Mockito.verify(rpcService).send("LoginEndpoint.Login", "George", "aaaaaa");
}