Java 单元测试困境
我正在开发一个类,它存储了一些会话信息以与第三方API通信。所以,基本上,它有很多行为和很少的状态需要维护。以下是其方法之一: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
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
}
}