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

标签 android unit-testing testing mocking mockito

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

我有一个 View 和一个 Presenter。 View 是一个愚蠢的 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 应该只用于模拟和验证外部服务。您确定您拥有它的方式是次优的,这是正确的,主要是因为您正在测试您的实现而不是您的总契约(Contract)

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

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

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

// 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);
}

不过,按照您编写它的方式,一个更有值(value)的策略可能是将整个 Presenter 视为您的被测单元并且仅测试您的 Presenter 的外部交互 .此时,对 attemptLogin 的调用应该是您的测试不关心的实现细节,这样您就可以随意重构它。

attemptLogin 在外部运行时会发生什么?也许这里的外部交互是您的 Presenter 使用正确的参数启动到路径 LoginEndpoint.Login 的 RPC。然后,不是验证演示器中的实现细节,而是验证它与外界的交互——这正是 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");
}

关于android - 为什么 Mockito 测试模拟而不是被测对象?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36127447/

相关文章:

android - 使用 Djinni 进行接口(interface)继承的替代方案

c - C 中的左值、右值和数组初始化

function - 您如何编写测试来测试具有可变返回值的函数?

java - 如何将使用命令行参数的java类转换为在android中使用?

android - Android future 的用例是什么?

unit-testing - 默认情况下,单元测试和集成测试使用相同的名称

java - 错误 : javax. persistence.JoinColumn.foreignKey()Ljavax/persistence/ForeignKey 与 Spring Controller

ruby-on-rails - Rails 形式测试 : post directly or through form?

java - @DataJpaTest : how to stop loading unneeded @ConfigurationProperties

android - 如何在android中加载不在System/lib文件夹中的.so文件?