在使用 Autofac 框架进行开发后,编写集成测试时,需要用 Mock 的用于测试的模拟的类型去代替容器里面已注入的实际类型,也就需要在 Autofac 完全收集完成之后,再次注入模拟的对象进行覆盖原有业务代码注册的正式对象。但 Autofac 默认没有提供此机制,我阅读了 Autofac 的源代码之后,创建了一些辅助代码,实现了此功能。本文将告诉大家如何在集成测试里面,在使用了 Autofac 的项目里面,在所有收集完成之后,注入用于测试的 Mock 类型,和 Autofac 接入的原理
背景
为什么选择使用 Autofac 框架?原因是在此前的 WPF 项目里面,有使用过的是 MEF 和 Autofac 两个框架,而 MEF 的性能比较糟心。解决 MEF 性能问题的是 VS-MEF 框架。在后续开发的一个 ASP.NET Core 项目里面,也就自然选用了 Autofac 框架
对比原生的 ASP.NET Core 自带的 DI 框架,使用 Autofac 的优势在于支持模块化的初始化,支持属性注入
默认的 Autofac 可以通过 Autofac.Extensions.DependencyInjection
将 Autofac 和 dotnet 通用依赖注入框架合入在一起,但在 Autofac 里面的定制要求是在 Startup 的 ConfigureContainer 函数里面进行依赖注入,也就是在默认的 ASP.NET Core 里面没有提供更靠后的依赖注入方法,可以在完成收集之后,再次注入测试所需要的类型,覆盖业务代码里面的实际对象
需求
假定在一个应用,如 ASP.NET Core 应用里面,进行集成测试,想要在集成测试里面,使用项目里面的依赖注入关系,只是将部分类型替换为测试项目里面的模拟的类型
而在应用里面,实际的业务类型是在 Autofac 的 Module 进行注入的。在应用里面的大概逻辑如下,在 Program 的 CreateHostBuilder 函数里面通过 UseServiceProviderFactory 方法使用 Autofac 替换 原生 的框架
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
// 使用 auto fac 代替默认的 IOC 容器
.UseServiceProviderFactory(new AutofacServiceProviderFactory());
在 Startup 里面添加 ConfigureContainer 方法,代码如下
public void ConfigureContainer(ContainerBuilder builder)
{
}
在 ConfigureContainer 里面注入具体的需要初始化的业务模块,例如 FooModule 模块。在具体模块里面注入实际业务的类型,如 Foo 类型,代码如下
public class Startup
{
// 忽略代码
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterModule(new FooModule());
}
}
class FooModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
Console.WriteLine($"06 FooModule");
builder.RegisterType<Foo>().As<IFoo>();
}
}
public class Foo : IFoo
{
}
public interface IFoo
{
}
现在的需求是在集成测试里面,将 IFoo 的实际类型从 Foo 替换为 TestFoo 类型
在集成测试项目里面,可以使用如下代码获取实际的项目的依赖注入收集
var hostBuilder = Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.UseTestServer(); //关键是多了这一行建立TestServer
})
// 使用 auto fac 代替默认的 IOC 容器
.UseServiceProviderFactory(new AutofacServiceProviderFactory());
var host = hostBuilder.Build();
var foo = host.Services.GetService<IFoo>();
以上的 foo 就是从收集的容器里面获取的 IFoo 对象,以上代码获取到的是业务代码的 Foo 类型对象。假定需要让容器里面的 IFoo 的实际类型作为测试的 TestFoo 类型,就需要在实际项目的依赖注入收集完成之前,进行测试的注入
但实际上没有在 Autofac 里面找到任何的辅助方法可以用来实现此功能。如果是默认的应用框架,可以在 ConfigureWebHostDefaults 函数之后,通过 ConfigureServices 函数覆盖在 Startup 的 ConfigureServices 函数注入的类型
实现方法
实现的方法是很简单的,关于此实现为什么能解决问题还请参阅下文的原理部分
集成测试项目不需要改动原有的业务项目即可完成测试,实现方法是在集成测试项目里面添加 FakeAutofacServiceProviderFactory 用来替换 Autofac 的 AutofacServiceProviderFactory 类型,代码如下
class FakeAutofacServiceProviderFactory : IServiceProviderFactory<ContainerBuilder>
{
public FakeAutofacServiceProviderFactory(
ContainerBuildOptions containerBuildOptions = ContainerBuildOptions.None,
Action<ContainerBuilder>? configurationActionOnBefore = null,
Action<ContainerBuilder>? configurationActionOnAfter = null)
{
_configurationActionOnAfter = configurationActionOnAfter;
AutofacServiceProviderFactory =
new AutofacServiceProviderFactory(containerBuildOptions, configurationActionOnBefore);
}
private AutofacServiceProviderFactory AutofacServiceProviderFactory { get; }
private readonly Action<ContainerBuilder>? _configurationActionOnAfter;
public ContainerBuilder CreateBuilder(IServiceCollection services)
{
return AutofacServiceProviderFactory.CreateBuilder(services);
}
public IServiceProvider CreateServiceProvider(ContainerBuilder containerBuilder)
{
_configurationActionOnAfter?.Invoke(containerBuilder);
return AutofacServiceProviderFactory.CreateServiceProvider(containerBuilder);
}
}
可以看到本质的 FakeAutofacServiceProviderFactory 实现就是通过 AutofacServiceProviderFactory 的属性实现,只是在 Cre