我正在编写一个组件,给定一个ZIP文件,需要:
我想对这个组件进行单元测试.
我很想编写直接处理文件系统的代码:
void DoIt()
{
Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
myDll.InvokeSomeSpecialMethod();
}
Run Code Online (Sandbox Code Playgroud)
但人们经常说,"不要编写依赖于文件系统,数据库,网络等的单元测试".
如果我以单元测试友好的方式写这个,我想它看起来像这样:
void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
string path = zipper.Unzip(theZipFile);
IFakeFile file = fileSystem.Open(path);
runner.Run(file);
}
Run Code Online (Sandbox Code Playgroud)
好极了!现在它是可测试的; 我可以将测试双打(模拟)提供给DoIt方法.但是以什么代价?我现在必须定义3个新接口才能使这个可测试.究竟,我在测试什么?我正在测试我的DoIt函数是否正确地与其依赖项交互.它不测试zip文件是否正确解压缩等.
我觉得我不再测试功能了.感觉就像我只是在测试课堂互动.
我的问题是:对依赖于文件系统的东西进行单元测试的正确方法是什么?
编辑我正在使用.NET,但这个概念也可以应用Java或本机代码.
最近我读过Mark Seemann关于Service Locator反模式的文章.
作者指出ServiceLocator为反模式的两个主要原因:
API使用问题(我完全可以使用)
当类使用服务定位器时,很难看到它的依赖关系,因为在大多数情况下,类只有一个PARAMETERLESS构造函数.与ServiceLocator相比,DI方法通过构造函数的参数显式地暴露依赖关系,因此在IntelliSense中很容易看到依赖关系.
维护问题(让我感到困惑)
请考虑以下示例
我们有一个使用服务定位器方法的类'MyType':
public class MyType
{
public void MyMethod()
{
var dep1 = Locator.Resolve<IDep1>();
dep1.DoSomething();
}
}
Run Code Online (Sandbox Code Playgroud)
现在我们要为类'MyType'添加另一个依赖项
public class MyType
{
public void MyMethod()
{
var dep1 = Locator.Resolve<IDep1>();
dep1.DoSomething();
// new dependency
var dep2 = Locator.Resolve<IDep2>();
dep2.DoSomething();
}
}
Run Code Online (Sandbox Code Playgroud)
这就是我的误解开始的地方.作者说:
要判断你是否引入了一个重大改变,要变得更加困难.您需要了解使用Service Locator的整个应用程序,并且编译器不会帮助您.
但是等一下,如果我们使用DI方法,我们将在构造函数中引入与另一个参数的依赖关系(在构造函数注入的情况下).问题仍然存在.如果我们忘记设置ServiceLocator,那么我们可能忘记在IoC容器中添加新的映射,并且DI方法将具有相同的运行时问题.
此外,作者还提到了单元测试的难点.但是,我们不会有DI方法的问题吗?我们不需要更新所有实例化该类的测试吗?我们将更新它们以传递一个新的模拟依赖项,以使我们的测试可编译.我没有看到更新和时间花费带来任何好处.
我不是想捍卫Service Locator方法.但这种误解让我觉得我失去了一些非常重要的东西.有人可以消除我的怀疑吗?
更新(摘要):
我的问题"服务定位器是反模式"的答案实际上取决于具体情况.我绝对不会建议你从工具列表中删除它.当您开始处理遗留代码时,它可能会变得非常方便.如果你很幸运能够处于项目的最初阶段,那么DI方法可能是更好的选择,因为它比Service Locator有一些优势.
以下是主要的不同之处,这些差异使我不相信我的新项目使用Service Locator:
有关详细信息,请阅读下面给出的优秀答案.
design-patterns dependency-injection anti-patterns service-locator
如果我理解正确,依赖注入的典型机制是通过类的构造函数或通过类的公共属性(成员)注入.
这暴露了注入的依赖性并违反了封装的OOP原则.
我在确定这种权衡时是否正确?你是如何处理这个问题的?
另请参阅下面我对自己问题的回答.
标准的Spring Web应用程序(由Roo或"Spring MVC Project"模板创建)使用ContextLoaderListener和创建一个web.xml DispatcherServlet.为什么他们不仅使用DispatcherServlet并使其加载完整的配置?
我知道ContextLoaderListener应该用于加载非Web相关的东西,而DispatcherServlet用于加载与Web相关的东西(Controllers,...).这导致两个上下文:父上下文.
背景:
几年来我一直以这种标准方式做这件事.
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:META-INF/spring/applicationContext*.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Handles Spring requests -->
<servlet>
<servlet-name>roo</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/spring/webmvc-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
Run Code Online (Sandbox Code Playgroud)
这通常会导致两个上下文及其之间的依赖关系出现问题.在过去,我始终能够找到解决方案,我强烈认为这使得软件结构/架构总是更好.但现在我面临着两种情境事件的问题.
- 然而这让我重新思考这两个上下文模式,我问自己:为什么我要把自己带入这个麻烦,为什么不用一个加载所有弹簧配置文件DispatcherServlet并ContextLoaderListener完全删除.(我仍然会有不同的配置文件,但只有一个上下文.)
有没有理由不删除ContextLoaderListener?
假设我们有一个班级:
public class MyClass {
@Autowired private AnotherBean anotherBean;
}
Run Code Online (Sandbox Code Playgroud)
然后我们创建了这个类的对象(或者其他一些框架创建了这个类的实例).
MyClass obj = new MyClass();
Run Code Online (Sandbox Code Playgroud)
是否仍然可以注入依赖项?就像是:
applicationContext.injectDependencies(obj);
Run Code Online (Sandbox Code Playgroud)
(我认为Google Guice有这样的东西)
我什么时候使用就有点糊涂了${...}比较#{...}.Spring的文档仅使用#{...},但有很多示例使用${...}.此外,当我开始使用SpEL时,我被告知要使用${...}它并且工作正常.
对于那些困惑的人来说,我将如何使用它
@Component
public class ProxyConfiguration {
@Value("${proxy.host}")
private String host;
@Value("${proxy.port}")
private String port;
:
}
Run Code Online (Sandbox Code Playgroud)
和一些属性文件:
proxy.host=myproxy.host
proxy.port=8000
Run Code Online (Sandbox Code Playgroud)
我的问题是:
在一些情况下,我最常使用"混蛋注射".当我有一个"适当的"依赖注入构造函数时:
public class ThingMaker {
...
public ThingMaker(IThingSource source){
_source = source;
}
Run Code Online (Sandbox Code Playgroud)
但是,对于我打算作为公共API(其他开发团队将使用的类)的类,我永远找不到比编写具有最可能需要的依赖项的默认"bastard"构造函数更好的选择:
public ThingMaker() : this(new DefaultThingSource()) {}
...
}
Run Code Online (Sandbox Code Playgroud)
这里明显的缺点是,这会对DefaultThingSource产生静态依赖; 理想情况下,没有这种依赖性,消费者总是会注入他们想要的任何IThingSource.但是,这太难用了; 消费者希望新建一个ThingMaker并开始制作物品,然后几个月后,在需要时注入其他东西.在我看来,这只留下了几个选项:
男孩,#3肯定看起来很有吸引力.还有另一种更好的选择吗?#1或#2似乎不值得.
(与此问题相关,EF4:为什么在启用延迟加载时必须启用代理创建?).
我是DI的新手,所以请耐心等待.我知道容器负责实例化我所有已注册的类型,但为了做到这一点,它需要引用我的解决方案中的所有DLL及其引用.
如果我没有使用DI容器,我就不必在我的MVC3应用程序中引用EntityFramework库,只需引用我的业务层,它将引用我的DAL/Repo层.
我知道在一天结束时所有的DLL都包含在bin文件夹中,但我的问题是必须通过VS中的"添加引用"显式引用它,以便能够发布包含所有必需文件的WAP.
我开始将我的asp.net核心RC1项目转换为RC2,并面临现在IHttpContextAccessor无法解决的问题.
为简单起见,我使用Visual Studio模板创建了新的ASP.NET RC2项目ASP.NET Core Web Application (.Net Framework).比我添加了HomeController的构造函数,为我创建了哪个模板.
public HomeController(IHttpContextAccessor accessor)
{
}
Run Code Online (Sandbox Code Playgroud)
在我开始申请后,我收到下一个错误:
InvalidOperationException:尝试激活"TestNewCore.Controllers.HomeController"时,无法解析类型"Microsoft.AspNetCore.Http.IHttpContextAccessor"的服务.Microsoft.Extensions.Internal.ActivatorUtilities.GetService(IServiceProvider sp,Type type,Type requiredBy,Boolean isDefaultParameterRequired)
在我的实际应用中,我需要解决IHttpContextAccessor我自己的服务类获得访问_contextAccessor.HttpContext.Authentication和_contextAccessor.HttpContext.User.在RC1中,Everething运行良好.那么怎么能想到RC2呢?
因为我一直在使用Spring,如果我要编写一个具有依赖项的服务,我会执行以下操作:
@Component
public class SomeService {
@Autowired private SomeOtherService someOtherService;
}
Run Code Online (Sandbox Code Playgroud)
我现在遇到了使用另一个约定来实现相同目标的代码
@Component
public class SomeService {
private final SomeOtherService someOtherService;
@Autowired
public SomeService(SomeOtherService someOtherService){
this.someOtherService = someOtherService;
}
}
Run Code Online (Sandbox Code Playgroud)
我理解这两种方法都有效.但是使用选项B有一些优势吗?对我来说,它在类和单元测试中创建了更多代码.(必须编写构造函数而不能使用@InjectMocks)
有什么我想念的吗?除了在单元测试中添加代码之外,还有其他任何自动装配的构造函数吗?这是一种更优先的依赖注入方式吗?
spring ×4
.net ×2
constructor ×2
java ×2
oop ×2
asp.net-core ×1
autowired ×1
c# ×1
dependencies ×1
servlets ×1
spring-el ×1
unit-testing ×1