java – 使代码可测试的优选方法:依赖注入与封装
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了java – 使代码可测试的优选方法:依赖注入与封装,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含6466字,纯文字阅读大概需要10分钟。
内容图文
我经常发现自己想知道这些问题的最佳实践是什么.一个例子:
我有一个java程序,应该从天气网络服务获得气温.我将其封装在一个类中,该类创建一个HttpClient并向天气服务执行Get REST请求.为类编写单元测试需要对HttpClient进行存根,以便可以接收伪数据.有几个选项如何实现这个:
构造函数中的依赖注入.这破坏了封装.如果我们切换到SOAP Web服务,则必须注入SoapConnection而不是HttpClient.
仅为测试目的创建setter.默认情况下构造“普通”HttpClient,但也可以通过使用setter来更改HttpClient.
反射.将HttpClient作为由构造函数设置的私有字段(但不是通过参数获取),然后让测试使用反射将其更改为存根字段.
打包私密.降低字段限制以使其在测试中可访问.
在试图阅读有关该主题的最佳实践时,在我看来,普遍的共识是依赖注入是首选方式,但我认为打破封装的缺点是没有给予足够的思考.
您认为什么是使类可测试的首选方法?
解决方法:
我认为最好的方法是通过依赖注入,但不是你描述的方式.而不是直接注入HttpClient,而是注入WeatherStatusService(或一些等效的名称).我会用一个方法(在你的用例中)getWeatherStatus()使这个简单的接口.然后,您可以使用HttpClientWeatherStatusService实现此接口,并在运行时注入此接口.要对核心类进行单元测试,您可以选择通过使用您自己的单元测试要求实现WeatherStatusService,或使用模拟框架来模拟getWeatherStatus方法来自行存取接口.这种方式的主要优点是:
>您不会破坏封装(因为更改为SOAP实现涉及创建SOAPWeatherStatusService并删除HttpClient处理程序).
>您已经打破了最初的单个类,现在有两个具有不同目的的类,一个类显式处理从API检索数据,另一个类处理核心逻辑.这可能是一个流程:接收天气状态请求(从更高的位置) – >请求从api检索数据 – >处理/验证返回的数据 – > (可选地)存储数据或触发其他过程以对数据进行操作 – >返回数据.
>如果出现不同的用例以利用此数据,您可以轻松地重复使用WeatherStatusService实现. (例如,也许您有一个用例来每4小时存储一次天气情况(向用户显示日期开发的交互式地图),以及另一个用例来获取当前天气.在这种情况下,您需要两个不同的核心逻辑要求都需要使用相同的API,因此在这些方法之间使API访问代码保持一致是有意义的.
这种方法被称为六角形/洋葱结构,我建议在这里阅读:
> http://alistair.cockburn.us/Hexagonal+architecture
> http://jeffreypalermo.com/blog/the-onion-architecture-part-1/
或者这篇文章总结了核心思想:
> http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
编辑:
继续你的意见:
What about testing the HttpClientWeatherStatus? Ignore unit testing or else we have to find a way to mock HttpClient there?
使用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’事件’在哪里:
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.
}
}
您通常会发现集成层中的条件和循环很少(因为这些构造代表逻辑,所有逻辑都应该在核心中).正因为如此(特别是因为在调用代码中只有一个条件分支路径),有些人会认为这个类没有点单元测试,并且它可以通过集成测试轻松覆盖,并且以一种不那么脆弱的方式.我理解这个观点,并且在集成层中跳过单元测试没有问题,但我个人无论如何都会对它进行单元测试.这是因为我相信集成域中的单元测试仍然可以帮助我确保我的类高度可用,可移植/可重用(如果它易于测试,那么它很容易从代码库中的其他地方使用).我还使用单元测试作为详细说明类的使用的文档,其优点是任何CI服务器都会在文档过期时提醒我.
Isn’t it bloating the code for a small problem which could have been “fixed” by just some lines using reflection or simply changing to package private field access?
将“固定”放在引号中这一事实说明了您认为这种解决方案的有效性. ;)我同意代码肯定存在一些膨胀,这一开始可能令人不安.但真正的要点是创建一个易于开发的可维护代码库.我认为一些项目开始很快,因为他们通过使用黑客和狡猾的编码实践来“解决”问题以保持速度.生产力往往停滞不前,因为压倒性的技术债务使得变化成为一个巨大的重新因素,需要数周甚至数月.
一旦您以六边形方式设置项目,当您需要执行以下操作之一时,即可获得真正的收益:
>更改其中一个集成层的技术堆栈. (例如从mysql到postgres).在这种情况下(如上所述),您只需实现一个新的持久层,确保使用绑定/事件/适配器层中的所有相关接口.不需要更改核心代码或界面.最后删除旧图层,并将新图层注入到位.
>添加新功能.通常,集成层已经存在,甚至可能不需要修改即可使用.在上面的getCurrentWeather()和store4HourlyWeather()用例的示例中.假设您已经使用上面列出的类实现了store4HourlyWeather()功能.要创建这个新功能(让我们假设该过程以一个宁静的请求开始),您需要创建三个新文件.您需要在Web层中使用新类来处理初始请求,您需要在核心层中使用新类来表示getCurrentWeather()的用户故事,并且您需要在绑定/事件/适配器层中使用接口作为核心类实现,并且Web类已注入其构造函数.现在一方面,是的,你已经创建了3个文件,只能创建一个文件,或者甚至只是将它添加到现有的restful web处理程序上.当然你可以,在这个简单的例子中,它可以正常工作.只是随着时间的推移,层之间的区别变得明显,重构变得困难.考虑在将其添加到现有类的情况下,该类不再具有明显的单一目的.你会怎么称呼它?怎么会有人知道这个代码?您的测试设置变得多么复杂,以至于您可以测试此类,因为有更多的依赖项需要模拟?
>更新集成层更改.继上面的示例之后,如果天气服务API(您从中获取信息)发生变化,则只有一个地方需要在程序中进行更改以便再次与新API兼容.这是代码中唯一知道数据实际来源的地方,因此它是唯一需要更改的地方.
>将项目介绍给新的团队成员.可以说,因为任何布局合理的项目都很容易理解,但到目前为止我的经验是大多数代码看起来简单易懂.它实现了一件事,并且非常擅长实现这一点.了解Amazon-S3相关代码的位置(例如)是显而易见的,因为有一整个层专门用于与之交互,而且该层中没有与其他集成问题相关的代码.
>修复错误.与上述相关联,通常可重复性是解决问题的最重要步骤.所有集成层都是不可变的,独立的,并且接受清晰的参数的优点是,很容易隔离单个故障层并修改参数直到它失败. (尽管如此,设计良好的代码也可以做得很好).
我希望我已经回答了你的问题,如果你有更多,请告诉我. :)也许我会考虑在周末创建一个样本六边形项目并在此链接以更清楚地展示我的观点.
内容总结
以上是互联网集市为您收集整理的java – 使代码可测试的优选方法:依赖注入与封装全部内容,希望文章能够帮你解决java – 使代码可测试的优选方法:依赖注入与封装所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。