单元测试困境
Unit testing dilemma
我正在研究 class,它存储了一些会话信息以与第三方 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
除此之外我无法做任何事情,因为我的状态没有变化对象和大多数对象交互都是第三方。
问题
这些场景中的大部分困惑是我的 class 应该对协作者了解多少?我的测试需要了解我的被测方法使用的对象的多少?
示例:
@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
一段时间后,我发现对 class 进行单元测试变得太困难了,因为对 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 服务,并根据需要转换结果。所以我会去进行集成测试。测试将调用 the/a SOAP 服务,但我会模拟它。 IE。您设置了一项服务,您可以在其中指定它将如何响应您的请求。然后调用该方法,并检查结果。
其他需要考虑的事情
我假设您已经使用单元测试测试了该方法中使用的所有内容。
让代码的 reader 感到困惑的一件事是 networkid
的一些奇怪处理,这可能会使测试变得比它需要的更难。它在 session
中设置为 'Code',这本身很奇怪,但没有被使用。好吧,实际上我假设某些东西正在从会话中获取该值,但这基本上是全局状态,因此很难推断正在发生的事情。如果您需要在全局状态下使用它以避免将它到处传递,请将该部分移出一个单独的方法(或将其余部分提取到一个新方法中),这样您就可以测试其他所有内容,而无需更改全局状态.或者将它明确地传递给实际需要它的方法。
我将从重构方法开始(仅通过提取私有方法并四处移动)使其看起来像这样:
public LineItem getLineItem(String networkId, String lineItemId) throws ApiException_Exception {
LineItemServiceInterface lineItemService = getLineItemServiceForNetwork(networkId);
return getFirstItemFromPage(lineItemService.getLineItemsByStatement(buildStatement(lineItemId)));
}
看看这个版本的代码,我们发现这个方法至少有一个太多的责任。例如,创建和设置 LineItemServiceInterface
应该卸载到可以模拟的协作者,或者它应该由调用者而不是 networkId
提供(因为如果调用者不提供您必须将服务提供商合作者模拟为 return 另一个模拟的服务)。如果将 LineItemServiceInterface
的创建卸载到另一个 class 太痛苦(因为很多遗留依赖项),一个快速而肮脏的替代方案是使 getLineItemServiceInterface()
受保护或包级别和将其覆盖为 return 您用于测试的子 class 中的模拟。
因此,对于 "normal usage" 情况,您对该方法的测试需要 1) 存根(使用 Mockito.when()
)当模拟服务接口接收到具有给定 [=18= 的正确格式的语句时],它 return 是一个包含一个 LineItem
实例的列表,然后 2) 检查所讨论的 LineItem
实例是否由 getLineItem()
编辑 return。那么你就知道getLineItem()
正确的调用了服务,并且正确的提取了结果。
顺便说一句,你不需要模拟 Statement
。您需要编写一个 matcher 来验证传递给 getLineItemsByStatement()
的 Statement
实例是否使用正确的 ID 值、顺序和限制正确制定。如果 Statement
是不允许访问此类信息的第三方 class(直接通过 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
}
}
我正在研究 class,它存储了一些会话信息以与第三方 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
除此之外我无法做任何事情,因为我的状态没有变化对象和大多数对象交互都是第三方。
问题
这些场景中的大部分困惑是我的 class 应该对协作者了解多少?我的测试需要了解我的被测方法使用的对象的多少?
示例:
@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
一段时间后,我发现对 class 进行单元测试变得太困难了,因为对 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 服务,并根据需要转换结果。所以我会去进行集成测试。测试将调用 the/a SOAP 服务,但我会模拟它。 IE。您设置了一项服务,您可以在其中指定它将如何响应您的请求。然后调用该方法,并检查结果。
其他需要考虑的事情 我假设您已经使用单元测试测试了该方法中使用的所有内容。
让代码的 reader 感到困惑的一件事是 networkid
的一些奇怪处理,这可能会使测试变得比它需要的更难。它在 session
中设置为 'Code',这本身很奇怪,但没有被使用。好吧,实际上我假设某些东西正在从会话中获取该值,但这基本上是全局状态,因此很难推断正在发生的事情。如果您需要在全局状态下使用它以避免将它到处传递,请将该部分移出一个单独的方法(或将其余部分提取到一个新方法中),这样您就可以测试其他所有内容,而无需更改全局状态.或者将它明确地传递给实际需要它的方法。
我将从重构方法开始(仅通过提取私有方法并四处移动)使其看起来像这样:
public LineItem getLineItem(String networkId, String lineItemId) throws ApiException_Exception {
LineItemServiceInterface lineItemService = getLineItemServiceForNetwork(networkId);
return getFirstItemFromPage(lineItemService.getLineItemsByStatement(buildStatement(lineItemId)));
}
看看这个版本的代码,我们发现这个方法至少有一个太多的责任。例如,创建和设置 LineItemServiceInterface
应该卸载到可以模拟的协作者,或者它应该由调用者而不是 networkId
提供(因为如果调用者不提供您必须将服务提供商合作者模拟为 return 另一个模拟的服务)。如果将 LineItemServiceInterface
的创建卸载到另一个 class 太痛苦(因为很多遗留依赖项),一个快速而肮脏的替代方案是使 getLineItemServiceInterface()
受保护或包级别和将其覆盖为 return 您用于测试的子 class 中的模拟。
因此,对于 "normal usage" 情况,您对该方法的测试需要 1) 存根(使用 Mockito.when()
)当模拟服务接口接收到具有给定 [=18= 的正确格式的语句时],它 return 是一个包含一个 LineItem
实例的列表,然后 2) 检查所讨论的 LineItem
实例是否由 getLineItem()
编辑 return。那么你就知道getLineItem()
正确的调用了服务,并且正确的提取了结果。
顺便说一句,你不需要模拟 Statement
。您需要编写一个 matcher 来验证传递给 getLineItemsByStatement()
的 Statement
实例是否使用正确的 ID 值、顺序和限制正确制定。如果 Statement
是不允许访问此类信息的第三方 class(直接通过 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
}
}