单元测试困境



>我正在研究一个存储一些会话信息以与第三方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))));
  }

基本方法

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

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

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

我将首先重构该方法(仅通过提取私有方法并移动内容)如下所示:

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) 检查有问题的LineItem实例是否由 getLineItem() 返回。然后,您知道getLineItem()正确调用服务并正确提取结果。

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

编辑:基于注释,下面是要编写的测试的粗略示例,假设进一步重构以卸载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 
      }
   }

相关内容

  • 没有找到相关文章

最新更新