Java 单元测试困境

Java 单元测试困境,java,unit-testing,tdd,mockito,guice,Java,Unit Testing,Tdd,Mockito,Guice,我正在开发一个类,它存储了一些会话信息以与第三方API通信。所以,基本上,它有很多行为和很少的状态需要维护。以下是其方法之一: public LineItem getLineItem( String networkId, String lineItemId) throws ApiException_Exception { LineItem lineItem = null; session.setCode(networkId); LineItemServi

我正在开发一个类,它存储了一些会话信息以与第三方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)

在我这方面,我试图模拟外部服务,并检查服务是否使用正确的
语句请求数据
,除此之外,我无法做任何事情,因为我的对象中没有状态更改,并且大多数交互的对象都是第三方的

问题

在这些场景中,最令人困惑的是我的类应该了解多少协作者?我的测试需要知道多少关于被测试方法使用的对象的信息

例如:

  @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
的处理有些奇怪。它在
会话中被设置为'code',这本身很奇怪,但它没有被使用。事实上,我假设某个东西正在从会话中获取该值,但这基本上是全局状态,因此很难对正在发生的事情进行推理。如果您需要在全局状态下使用它以避免到处传递,请使用单独的方法将该部分移出(或使用新方法提取其余部分),这样您就可以在不更改全局状态的情况下测试其他所有内容。或者将它明确地传递给实际需要它的方法。

基本方法

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

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


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

我将首先重构该方法(仅通过提取私有方法和移动对象),使其看起来像这样:

public LineItem getLineItem(String networkId, String lineItemId) throws ApiException_Exception {
    LineItemServiceInterface lineItemService = getLineItemServiceForNetwork(networkId);
    return getFirstItemFromPage(lineItemService.getLineItemsByStatement(buildStatement(lineItemId)));
}
   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 
      }
   }
看看这个版本的代码,我们发现这个方法至少有一个太多的职责。例如,创建和设置
LineItemServiceInterface
应该卸载给可以模拟的协作者,或者可能应该由调用者提供,而不是
networkId
(因为如果调用者不提供服务,则必须模拟服务提供者协作者以返回另一个模拟)。如果将
LineItemServiceInterface
的创建转移到另一个类太痛苦(因为有很多遗留依赖),那么一个快速而肮脏的替代方法是将
getLineItemServiceInterface()
设为受保护或包级别,并覆盖它以在用于测试的子类中返回模拟

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

顺便说一句,您不需要模拟
语句
。您需要编写一个验证传递给
getLineItemsByStatement()
语句
实例是否使用正确的ID值、顺序和限制正确制定。如果
语句
是第三方类,那么
   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 
      }
   }