【问题标题】:How should I handle a UnnecessaryStubbingException that is sensitive to ordering in underlying data structures?我应该如何处理对底层数据结构中的排序敏感的 UnnecessaryStubbingException?
【发布时间】:2018-08-16 14:41:49
【问题描述】:

我有一个测试,希望在发现用户被挂起时引发异常。

  @Test(expected = SuspendedException.class)
  public void testGetUserKeychain_WhenOneUserSuspended_ShouldThrowSuspended() throws Throwable {
    when(userKeychain.getUserStatus()).thenReturn(UserState.OK);
    when(otherUserKeychain.getUserStatus()).thenReturn(UserState.SUSPENDED);
    when(keyLookup.lookupKeychainsByUserId(any()))
        .thenReturn(CompletableFuture.completedFuture(ImmutableMap.copyOf(multiUserKeychains)));
    try {
      padlockUtil.getKeychains(
          Sets.newSet("userid", "otheruserid")).toCompletableFuture().get();
    } catch (ExecutionException e) {
      throw e.getCause();
    }
  }

但我得到的例外是:

org.mockito.exceptions.misusing.UnnecessaryStubbingException: 
Unnecessary stubbings detected in test class: PadlockUtilTest
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
  1. -> at com.xyz.server.padlock.PadlockUtilTest.testGetUserKeychain_WhenOneUserSuspended_ShouldThrowSuspended(PadlockUtilTest.java:119)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.

我相信这是因为在PadlockUtil::getKeychains 中,暂停的用户 OK 用户之前遇到,所以 Mockito 抱怨 OK 用户不需要被存根。

因为如果我交换谁被停职而不是……

when(userKeychain.getUserStatus()).thenReturn(UserState.SUSPENDED);
when(otherUserKeychain.getUserStatus()).thenReturn(UserState.OK);

...然后Mockito很高兴。而不是仅仅切换"userid""otheruserid";那里Mockito仍然不高兴,大概是因为那不是后来确定顺序的地方。

在我设置的这个特定示例中,可能没有必要存根第一个用户。但这在未来可能会产生误导;我希望存根存在,这不是因为“宽容”,IMO。我也可以暂停第一个用户而不是第二个用户,但它没有明确解决这个微妙之处,而且它可能会在以后再次出现,让开发人员感到困惑。

这样做的正确方法是什么,这样底层的操作顺序(我们在这里处理集合和映射,仅此而已)不是测试中的一个因素?

【问题讨论】:

  • 你应该发布测试代码的相关部分。

标签: java exception java-8 mockito


【解决方案1】:

Lenient mocks 是你想要的,如果你不能只使用真正的 UserKeychain。

Mockito.lenient().when(userKeychain.getUserStatus()).thenReturn(UserState.OK);
Mockito.lenient().when(otherUserKeychain.getUserStatus()).thenReturn(UserState.SUSPENDED);

Mockito 旨在替换无法在测试中使用真实系统的系统,尤其是在可预测地调用服务而不是从数据对象中获取属性的系统中(或其他幂等动作)。因为您的系统不会以确定的顺序调用这些方法,而且调用成本不高且没有副作用,所以我建议只使用“宽松”选项。


想象一下这种情况,您正在测试删除用户1001

when(userRpc.deleteUser(1001)).thenReturn(RPC_SUCCESS);
when(userRpc.deleteUser(1002)).thenReturn(RPC_SUCCESS);  // unnecessary

如果您删除了错误的用户,测试可能会通过:过度存根掩盖了问题。比较一下:

when(userRpc.fetchExpensiveUserDetails(1001)).thenReturn(user1001);
when(userRpc.fetchExpensiveUserDetails(1002)).thenReturn(user1002);  // unnecessary

根据您正在测试的内容,这可能很危险,但可能不会那么糟糕。模拟缓慢的移动网络或使用昂贵的数据,可能完全违反规范,让您获取太多。但是,在其他情况下,它可能是可以接受的。最后,对比一下这个案例:

when(calculationResult.getRealComponent()).thenReturn(-1d);
when(calculationResult.getComplexComponent()).thenReturn(5);
when(calculationResult.getShortString()).thenReturn("-1 + 5i");

calculationResult 看起来非常像一个数据对象,它可能不是测试中要调用哪些方法或是否调用所有方法的关键部分。这是 Mockito 严格的 stubing 会阻碍您而不是帮助您的情况,并且可能是您想让其中一些 stubbing 宽松的情况。您也可以选择让整个 mock 宽松,如果您要创建一个像 stubCalculationResult(-1, 5) 这样为您准备整个对象的测试助手方法,这尤其有意义。

唯一比这更好的选择是使用一个真实的对象。在我的示例中,如果 CalculationResult 是一个现有的明确定义的对象,那么使用真实对象的总体风险可能低于模拟 您在编写测试时认为是正确的行为的风险。同样,对于您的情况,如果您有权访问填充 UserStatus 等的 UserKeychain 构造函数,那么在测试中使用它可能更安全。

虽然这可能乍一看是一个将单元测试转变为集成测试的滑坡,但我想澄清一下,我只为数据推荐这个对象,它们没有依赖关系,理想情况下是不可变对象,没有带有副作用的方法。如果您使用依赖注入,这些是您可以调用new 而不是从您的图表中获取的单一实现数据持有者的类型。这也是一个很好的理由来分离你的数据对象,使它们是不可变的并且易于构造,并且将你的服务转移到使用这些对象而不是给数据对象提供方法(赞成loginService.login(user)而不是user.login(loginService))。

【讨论】:

    【解决方案2】:

    我在类中使用了@MockitoSettings(strictness = Strictness.LENIENT) 注释,该注释引发了不必要的存根异常。它已解决“请删除不必要的存根或使用'宽松'严格性”错误。

    【讨论】:

      猜你喜欢
      • 2011-04-21
      • 1970-01-01
      • 1970-01-01
      • 2023-02-15
      • 2015-08-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多