我想为我的Asp .net核心应用程序编写集成测试,但我不希望我的测试使用某些服务的实际实现。
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
services.AddTransient<IExternalService,ExternalService>();
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
}
}
public interface IExternalService
{
bool Verify(int id);
}
public class ExternalService : IExternalService
{
public bool Verify(int id)
{
//Implemetation is here.
//I want to fake this implemetation during testing.
}
}
[Fact]
public void TestCase()
{
//Stub out service
var myExtService = new Mock<IExternalService>();
//Setup response by stub
myExtService
.Setup(p => p.Verify(It.IsAny<int>()))
.Returns(false);
var host = new WebHostBuilder()
.UseStartup<Startup>()
.ConfigureServices((services) =>
{
//Setup injection
services.AddTransient<IExternalService>((a) =>
{
return myExtService.Object;
});
});
var server = new TestServer(host);
var client = server.CreateClient();
var response = client.GetAsync("/").Result;
var responseString = response.Content.ReadAsStringAsync().Result;
Assert.Contains("Your service returned: False", responseString);
}
测试用例中的当前注入设置不起作用,因为通过模拟注入了ExternalService。
但是,当我从services.AddTransient<IExternalService,ExternalService>;
移除Startup
时,测试将会通过。
很可能稍后调用Startup
中的那个,并且应用程序首选该类中的所有设置。
我有哪些选项可以在测试中设置一些依赖项,但是使用Startup
中声明的所有其他内容?
更新
答案 0 :(得分:1)
我所知道的唯一选择是使用WebHostBuilder
设置UseEnvironment
:
var host = new WebHostBuilder()
.UseStartup<Startup>()
.ConfigureServices(services =>
{
//Setup injection
services.AddTransient<IExternalService>(provider =>
{
return myExtService.Object;
});
})
.UseEnvironment("IntegrationTest");
然后在ConfigureServices
中的Startup
方法中添加条件:
public void ConfigureServices(IServiceCollection services)
{
if (Configuration["Environment"] != "IntegrationTest")
{
services.AddTransient<IExternalService, ExternalService>();
}
services.AddMvc();
// ...
}
我做了一些更多的讨论,另一个选择是不使用UseStartup
扩展方法,而是直接配置WebHostBuilder
。您可以通过多种方式执行此操作,但我认为您可以创建自己的扩展方法来在测试中创建模板:
public static class WebHostBuilderExt
{
public static WebHostBuilder ConfigureServicesTest(this WebHostBuilder @this, Action<IServiceCollection> configureServices)
{
@this.ConfigureServices(services =>
{
configureServices(services);
services.AddMvc();
})
.Configure(builder =>
{
builder.UseMvc();
});
return @this;
}
}
现在您的测试可以设置如下:
var host = new WebHostBuilder()
.ConfigureServicesTest(services =>
{
//Setup injection
services.AddTransient<IInternalService>(provider =>
{
return myExtService.Object;
});
});
var server = new TestServer(host);
这意味着您必须显式设置容器将为您调用的特定端点解析的所有实现。您可以选择模拟或使用具体实现。
答案 1 :(得分:1)
经过数小时的研究后,我找到了解决方案。
我找不到单独使用内置依赖注入解决方案的方法,因此我选择了第三方DI解决方案 - Autofac
想法是使用WebHostBuilder(声明主程序)并添加必要的选项,以便在测试期间伪造一些服务。
我学到的东西:
host.UseStartup<Startup>
host.ConfigureServices()
host.UseStartup<Startup>(new Dependency())
中注册了相关性,那么它将在创建启动之前解析,并使用构造函数host.ConfigureServices(services => services.AddTransient<IDependency, MyDependency>())
创建public Startup(IDependency dependency)
。我的申请方:
Startup
测试用例:
public class Program
{
public static void Main(string[] args)
{
CreateWebHost(args)
.Build()
.Run();
}
public static IWebHostBuilder CreateWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureServices((services) =>
{
//Setup autofac.
services.AddAutofac();
//Register module dependency that Startup requires.
services.AddTransient<Module, MyAutofacModule>();
////It would a bit cleaner to use autofac to setup Startup dependency,
////but dependency did not get resolved for Startup.
//services.AddAutofac((builder) =>
//{
// builder.RegisterModule(new AutofacModule());
//});
})
.UseStartup<Startup>();
}
public class MyAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
//Register all application dependencies in this module.
builder.Register((c) => new ExternalService()).As<IExternalService>();
}
}
public class Startup
{
private Module applicationDIModule;
public Startup(Module applicationDIModule)
{
this.applicationDIModule = applicationDIModule;
}
public void ConfigureServices(IServiceCollection services)
{
//We can add build-in services such as mvc and authorization,
//but I would not use Add(Transient/Scoped/Singleton) here.
//You should register domain specific dependecies in MyAutofacModule,
//since it will be added after this method call.
services.AddMvc();
}
//This method is called after ConfigureServices (refer to Autofac link).
public void ConfigureContainer(ContainerBuilder builder)
{
//We will register injected module.
builder.RegisterModule(applicationDIModule);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvcWithDefaultRoute();
}
}
虽然感觉有点脆,但我仍然更喜欢这个解决方案而不是@ODawg所建议的。他的解决方案可行,但我发现在将来添加新的测试用例时会引起麻烦。
答案 2 :(得分:1)
您唯一需要更改的就是使用ConfigureTestServices
而不是ConfigureServices
。 ConfigureTestServices
在Startup
之后运行,因此您可以使用模拟/存根替换实际的实现。 ConfigureServices
是用于此目的的较新版本,相反,它配置“主机服务”,该主机服务在应用程序的主机构建阶段使用,并复制到应用程序的DI容器中。
ConfigureTestServices
在ASP Core 2.1及更高版本中可用。
var host = new WebHostBuilder()
.UseStartup<Startup>()
.ConfigureTestServices((services) =>
{
//Setup injection
services.AddTransient<IExternalService>((a) =>
{
return myExtService.Object;
});
});