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

时间:2016-03-21 09:39:23

标签: android unit-testing testing mocking mockito

我刚刚开始使用Mockito,我发现它并不特别有用。

我有一个视图和一个演示者。视图是一个愚蠢的活动,演示者包含所有业务逻辑。我想模拟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,但这对我来说似乎毫无用处。我该怎么办?

1 个答案:

答案 0 :(得分:1)

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

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

从技术角度来看,Mockito只能存储和验证模拟方法。这是因为,在表面之下,Mockito无法进入并检查系统中每个班级之间的每次互动;它的"模拟"是子类或代理,它们有效地覆盖每个单独的方法来记录交互以进行验证,并以您已经存根的方式进行响应。这意味着,如果您要调用whenverify,因为它适用于您的演示者,它需要在模拟或间谍对象上的非最终非静态方法,并且您&#39 ;请注意,这样可以很容易地无意中测试Mockito的工作情况,而不是测试您的设备或系统是否正常工作。

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

// 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使用正确的参数将RPC启动到路径LoginEndpoint.Login。然后,不是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");
}