【问题标题】:Unit testing dilemma单元测试困境
【发布时间】:2016-03-10 06:16:26
【问题描述】:

我正在研究一个存储一些会话信息以与第三方 API 通信的类。所以,基本上它有很多行为和很少的状态需要维护。这是它的一种方法:

  public LineItem getLineItem(
      String networkId, String lineItemId) throws ApiException_Exception {
    LineItem lineItem = null;
    session.setCode(networkId); 
    LineItemServiceInterface lineItemService = servicesInterface.lineItemService(session);
    StatementBuilder statementBuilder =
        new StatementBuilder()
            .where("id = " + lineItemId.trim())
            .orderBy("id ASC")
            .limit(StatementBuilder.SUGGESTED_PAGE_LIMIT);
    LineItemPage lineItemPage =
        lineItemService.getLineItemsByStatement(statementBuilder.toStatement());
    if (lineItemPage != null && lineItemPage.getResults() != null) {
      lineItem = lineItemPage.getResults().get(0);
    }
    return lineItem;
  }

我被困在如何测试这个方法上,它对第三方对象有太多的隐式依赖。这些对象很难自己创建。另一个大问题是getLineItemByStatement 在幕后进行网络调用(SOAP)。

就我而言,我正在尝试模拟外部服务并检查服务是否使用正确的Statement 请求数据,除此之外我无法做任何事情,因为我的对象没有状态更改,并且大部分交互的对象是第三方。

问题

在这些情况下,最令人困惑的是我的班级应该对合作者了解多少?我的测试需要了解多少关于我的测试方法使用的对象?

示例:

  @Test
  public void shouldGetLineItem() throws ApiException_Exception {
    when(servicesInterface.lineItemService(dfpSession)).thenReturn(mockLineItemService);
    dfpClient.getLineItem("123", "123");
    Statement mockStatement = mock(Statement.class);
    Statement statement =
        new StatementBuilder()
            .where("id = 123")
            .orderBy("id ASC")
            .limit(StatementBuilder.SUGGESTED_PAGE_LIMIT)
            .toStatement();

    verify(dfpSession).setNetworkCode("123");
    verify(mockLineItemService).getLineItemsByStatement(isA(Statement.class));
  }

我们可以看到我的测试对我的测试方法了解太多。

更新 1

一段时间后,我发现对我的类进行单元测试变得太难了,因为对 LineItem 的引用分散在各处,而且由于 LineItem 与其他对象有很多深层链接,因此很难创建自己的对象,因此我有决定创建一个域模型,其中包含我的应用程序的相关详细信息。

  public LineItemDescription getLineItem(String networkId, String lineItemId)
      throws ApiException_Exception {
    dfpSession.setNetworkCode(networkId);
    LineItemServiceInterface lineItemService = servicesInterface.lineItemService(dfpSession);
    return buildLineItemDescription(
        getFirstItemFromPage(lineItemService.getLineItemsByStatement(buildStatement(lineItemId))));
  }

【问题讨论】:

  • Mocking 对我来说似乎是正确的答案。具体问题是什么?
  • @JBNizet 目前更新了问题和单元测试。

标签: java unit-testing tdd mockito guice


【解决方案1】:

基本方法

这看起来像是我认为单元测试价值有限的情况。看来您真正想要的可能是一个测试,以确保正确调用 SOAP 服务,并根据需要转换结果。所以我会去进行集成测试。测试将调用 /a SOAP 服务,但我会模拟它。 IE。您设置了一项服务,您可以在其中指定它将如何响应您的请求。然后调用方法,检查结果。

需要考虑的其他事项 我假设您已经使用单元测试测试了该方法中使用的所有内容。

让代码的读者感到困惑并且可能使测试变得更加困难的一件事是对networkid 的处理有些奇怪。它在session 中设置为“代码”,这本身很奇怪,但没有被使用。好吧,实际上我假设某些东西正在从会话中获取该值,但这基本上是全局状态,因此很难推理正在发生的事情。如果您需要在全局状态下使用它以避免到处传递它,请在单独的方法中将该部分移出(或在新方法中提取其余部分),这样您就可以测试其他所有内容,而无需更改全局状态.或者将它明确地传递给实际需要它的方法。

【讨论】:

  • 设置代码后,在下一行传递session对象来获取lineItem服务所以,基本上lineItem服务依赖于会话。由于它是第三方 API 的设计方式,我无法对其进行重构。
【解决方案2】:

我会首先重构方法(只是通过提取私有方法并移动东西)看起来像这样:

public LineItem getLineItem(String networkId, String lineItemId) throws ApiException_Exception {
    LineItemServiceInterface lineItemService = getLineItemServiceForNetwork(networkId);
    return getFirstItemFromPage(lineItemService.getLineItemsByStatement(buildStatement(lineItemId)));
}

查看这个版本的代码,我们看到这个方法至少有一个太多的职责。例如,创建和设置LineItemServiceInterface 应该卸载到可以模拟的协作者,或者它应该由调用者而不是networkId 提供(因为如果调用者不提供您拥有的服务模拟服务提供者合作者返回另一个模拟)。如果将 LineItemServiceInterface 的创建卸载到另一个类太痛苦(因为有很多遗留依赖项),一个快速而肮脏的替代方法是使 getLineItemServiceInterface() 受保护或包级别并覆盖它以在用于测试的子类。

因此,对于“正常使用”情况,您对此方法的测试需要 1) 存根(使用 Mockito.when()),当模拟服务接口接收到具有给定 lineItemId 的正确格式语句时,它会返回一个列表,其中包含一个LineItem 实例,然后2) 检查getLineItem() 是否返回了有问题的LineItem 实例。那么你就知道getLineItem()正确调用了服务并正确提取了结果。

顺便说一句,您不需要模拟 Statement。您需要编写一个matcher 来验证传递给getLineItemsByStatement()Statement 实例是否使用正确的ID 值、排序和限制正确制定。如果Statement 是一个不允许访问此类信息的第三方类(直接通过getter 或间接通过生成的查询代码),您可以考虑将Statement 创建卸载到另一个注入的协作者,您将对其进行模拟这个测试,然后您在其他地方使用针对真实服务的集成测试来验证该协作者。

编辑:基于 cmets,这是一个粗略的测试示例,假设进一步重构以卸载 LineItemServiceInterface 设置协作者:

@Test
  public void shouldGetLineItem() throws ApiException_Exception {
    when(lineItemserviceProviderMock.getLineItemService(NETWORK_ID, dfpSession)).thenReturn(mockLineItemService);     
    when(mockLineItemService.getLineItemsByStatement(argThat(statementMatcher)).thenReturn(LIST_WITH_EXPECTED_LINE_ITEM);
    LineItem expectedResult = dfpClient.getLineItem(NETWORK_ID, LINE_ITEM_ID);
    assertEquals(EXPECTED_LINE_ITEM, expectedResult);
  }

测试中的变量statementMatcher 大致如下:

   ArgumentMatcher<Statement> statementMatcher = new ArgumentMatcher<Statement>{
      public boolean matches(Object stmt) {
          return queryMatches(((Statement)stmt).getQuery()) && valuesMatch(((Statement)stmt).getValues());
      }

      private boolean queryMatches(String query) {
        return EXPECTED_QUERY.equals(query);
      }

      private boolean valuesMatch(String_ValueMapEntry[] values) {
        // TODO: verify values here 
      }
   }

【讨论】:

  • servicesInterface 完全按照您所说的进行。我已经将原始服务创建封装在我的协作者类中,您也可以在我的测试中看到我也模拟了该服务,所以我们在这个问题上处于同一页面。其次,我同意我们可以进一步制作私有方法,但是我们如何测试它们呢?或者如何存根他们的返回值?
  • 另外,seesion 是您刚刚给出的lineItemService networkId 的强制参数。如果您为上述方法提供示例测试用例,我会更有帮助。
  • 关于session,没有传入getLineItem()方法,所以我没有传入getLineItemServiceForNetwork()。这并不意味着它没有被使用——但你不需要在getLineItem() 中了解它,它不应该负责设置``lineItemService`。
  • session 我已设置为实例属性。有关更多信息,您可以查看完整示例 here
  • 关于提供示例测试用例,我在回答中描述了“正常使用”测试应该做什么(您可能需要其他测试来处理空结果或错误输入)。不知道您的第 3 方库的详细信息,甚至您的被测类的名称,编写确切的测试代码有点棘手。
猜你喜欢
  • 2011-08-30
  • 2012-12-19
  • 2014-03-12
  • 2014-01-17
  • 1970-01-01
  • 2021-12-15
  • 1970-01-01
  • 1970-01-01
  • 2015-03-24
相关资源
最近更新 更多