我经常发现自己想知道这些问题的最佳实践是什么。一个例子:
我有一个Java程序,应该从天气Web服务获取气温。我将其封装在一个类中,该类创建HttpClient并向天气服务发出Get REST请求。为该类编写单元测试需要对HttpClient存根,以便可以代替地接收伪数据。有som选项如何实现此目的:
构造函数中的依赖注入。 这会破坏封装。如果我们改为切换到SOAP Web服务,则必须注入SoapConnection而不是HttpClient。
仅出于测试目的创建设置器。 默认情况下构造“正常” HttpClient,但是也可以使用设置器更改HttpClient。
反射Reflection 。 将HttpClient作为私有字段由构造函数设置(但不能通过参数设置),然后让测试使用反射将其更改为存根。
私有包。 降低字段限制以使其在测试中可以访问。
在尝试阅读有关该主题的最佳实践时,在我看来,通常的共识是依赖注入是首选的方式,但是我认为打破封装的缺点并未得到足够的考虑。
您认为使类可测试的首选方法是什么?
最佳答案
我相信最好的方法是通过依赖项注入,但是不完全是您描述的方法。而不是直接注入HttpClient
,而是注入WeatherStatusService
(或某些等效名称)。我将使用一种方法(在您的用例中)getWeatherStatus()
使其成为一个简单的接口。然后,您可以使用HttpClientWeatherStatusService
实现此接口,并在运行时将其注入。要对核心类进行单元测试,您可以选择通过自己的单元测试要求实现WeatherStatusService
或使用模拟框架来模拟getWeatherStatus
方法,从而对接口进行stub测试。这种方式的主要优点是:
SOAPWeatherStatusService
和删除HttpClient处理程序)。 WeatherStatusService
实现。 (例如,也许您有一个用例每隔4小时存储一次天气状况(向用户显示一天的动态变化图),而另一个用例则可以获取当前天气。在这种情况下,您需要两个两者都需要使用相同API的不同核心逻辑要求,因此使这些方法之间的API访问代码保持一致是很有意义的)。 这种方法被称为六角形/洋葱结构,我建议在这里阅读:
或这篇文章总结了核心思想:
编辑:
除了您的评论:
如何测试HttpClientWeatherStatus?忽略单元测试,否则我们必须找到一种模拟HttpClient的方法?
用
HttpClientWeatherStatus
类。理想情况下,它应该是不可变的,因此HttpClient
依赖项会在创建时注入到构造函数中。这使单元测试变得容易,因为您可以模拟HttpClient
并防止与外界的任何交互。例如:public class HttpClientWeatherStatusService implements WeatherStatusService {
private final HttpClient httpClient;
public HttpClientWeatherStatusService(HttpClient httpClient) {
this.httpClient = httpClient;
}
public WeatherStatus getWeatherStatus(String location) {
//Setup request.
//Make request with the injected httpClient.
//Parse response.
return new WeatherStatus(temperature, humidity, weatherType);
}
}
返回的
WeatherStatus
'Event'是:public class WeatherStatus {
private final float temperature;
private final float humidity;
private final String weatherType;
//Constructor and getters.
}
然后测试看起来像这样:
public WeatherStatusServiceTests {
@Test
public void givenALocation_WhenAWeatherStatusRequestIsMade_ThenTheCorrectStatusForThatLocationIsReturned() {
//SETUP TEST.
//Create httpClient mock.
String location = "The World";
//Create expected response.
//Expect request containing location, return response.
WeatherStatusService service = new HttpClientWeatherStatusService(httpClient);
//Replay mock.
//RUN TEST.
WeatherStatus status = service.getWeatherStatus(location);
//VERIFY TEST.
//Assert status contains correctly parsed response.
}
}
通常,您会发现在集成层中几乎没有条件和循环(因为这些构造代表逻辑,并且所有逻辑都应在核心中)。因此(特别是因为在调用代码中只有一条条件分支路径),一些人会认为几乎没有点单元测试此类,并且集成测试可以轻松地对其进行覆盖,并且以一种不太脆弱的方式。我理解这种观点,并且在集成层中跳过单元测试没有问题,但是我个人还是会对其进行单元测试。这是因为我相信集成域中的单元测试仍然可以帮助我确保 class 的可用性和可移植性/可重用性(如果易于测试,则可以从代码库的其他位置轻松使用)。我还将单元测试用作详细说明该类用法的文档,其优势在于,当文档过时时,任何CI服务器都会提醒我。
难道不是因为一个小问题而使代码膨胀,该小问题本可以通过使用反射或仅更改为打包私有字段访问的几行代码来“修复”的?
您在引号中加上“固定”的事实充分说明了您认为这种解决方案的有效性。 ;)我同意代码肯定会有些膨胀,乍一看这可能会令人不安。但是,真正的意义是制作易于开发的可维护代码库。我认为有些项目起步很快,因为它们通过使用黑客和狡猾的编码实践来“解决”问题以保持步伐。由于压倒性的技术债务带来了变化,生产力通常会停顿下来,这应该是猛烈的重构的衬托,这种重构需要数周甚至数月的时间。
一旦您以六边形方式设置了项目,那么当您需要执行以下一项操作时,真正的收获就来了:
getCurrentWeather()
和store4HourlyWeather()
用例示例中。假设您已经使用上面概述的类实现了store4HourlyWeather()
功能。要创建此新功能(假设过程以一个轻松的请求开始),您需要制作三个新文件。在Web层中需要一个新类来处理初始请求,在核心层中需要一个新类来表示getCurrentWeather()
的用户故事,并且在绑定/事件/适配器层中需要一个由核心类实现的接口,并且网络类已将其注入构造函数。现在,是的,是的,您只可能创建一个文件,甚至只是将其添加到现有的静态Web处理程序上就创建了3个文件。您当然可以,并且在这个简单的示例中可以正常工作。只是随着时间的推移,层之间的区别才变得明显,重构也变得困难。考虑将其添加到现有类上的情况,该类不再具有明显的单一目的。你会怎么称呼它?谁会知道要在其中查找此代码?您的测试设置变得多么复杂,以便现在可以模拟更多的依赖项,从而可以测试此类? 希望我已经回答了您的问题,如果还有其他问题,请告诉我。 :)也许我会考虑在周末创建一个六角形示例项目,并将其链接到此处以更清楚地说明我的观点。
关于java - 使代码可测试的首选方法:依赖注入(inject)与封装,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23056418/