只验证发生状态变化的方法调用

乔梁 | 2021-02-11

只验证发生状态变化的方法调用

某个对象的方法调用分为两类

  • 引起状态变化的:有副作用,并且改变了被测代码之外的信息,例如 sendemail()、saverecord()、logaccess() 。

  • 未引起状态变化的:只是返回被测代码之外的信息,但并不修改任何内容的方法,例如 getuser()、findresults()、readfile()。

应避免验证那些并未修改状态的方法是否被调用了。这种验证通常:

  • 是多余的:一个不改变外部世界状态的方法调用,对其本身来说是没有什么意义的。测试中的代码将使用方法调用的返回值来执行您可以断言的其他工作。

  • 使测试变得脆弱:只要方法调用发生变化,就要更新测试代码。例如,如果一个测试期望的是调用 mockuserservice.isUserActive(user) ,那么,当将被测代码被修改为调用 user.isActive() 时,这个测试就会失败。

  • 使测试的可读性降低:测试中附加的断言让“确认到底是哪个方法调用真正影响了世界状态”变得更加困难。

  • 给出了一种错误的安全感:仅仅因为被测试的代码调用了一个方法,并不意味着被测试的代码用该方法的返回值做了正确的事情。

不要验证是否调用了它们,而是使用非状态更改方法来模拟测试中的不同条件,例如,

when(mockUserService.isUserActive(USER)).thenReturn(false)

然后为被测代码的返回值编写断言,或者验证那个引起状态变化的方法被调用了。

假如没有可以断言的其他输出,那么,对不更改状态的方法进行验证也是很有用的。

例如,如果您的代码应该缓存一个 RPC 结果,您可以验证使用该 RPC 的方法只被调用一次。

下面的哪一行代码可以被安全地移除?

@Test public void addPermissionToDatabase() {
  new UserAuthorizer(mockUserService, mockPermissionDb).grantPermission(USER, READ_ACCESS);

  // The test will fail if any of these methods is not called.
  verify(mockUserService).isUserActive(USER);
  verify(mockPermissionDb).getPermissions(USER);
  verify(mockPermissionDb).isValidPermission(READ_ACCESS);
  verify(mockPermissionDb).addPermission(USER, READ_ACCESS);
}

答案是:前三个 verify 。

删除没必要的代码之后,测试应该如下所示:

@Test public void addPermissionToDatabase() {
  new UserAuthorizer(mockUserService, mockPermissionDb).grantPermission(USER, READ_ACCESS);

  // Verify only the state-changing method.
  verify(mockPermissionDb).addPermission(USER, READ_ACCESS);
}

这样是不是简单多啦!但是要记住,与其使用 mock 来验证是否调用了方法,不如使用一个真实的对象或者 fake 来实际执行该方法,并检查其是否正常工作。例如,上面的测试可以使用一个假数据库来检查权限是否存在于数据库中,而不只是验证是否调用了addPermission()

你可以在《 Growing Object-Oriented Software, Guided by Tests》一书中找到更多的相关信息。要注意的是,这本书中使用的术语“command” 和 “query”,而不是“state-changing”或 “non-state-changing”.