Xunit.DependencyInjection
改造測試項目這篇文章拖了很長時間沒寫,以前也有介紹過 Xunit.DependencyInjection
這個項目,這個項目是由大師寫的一個 Xunit
基於微軟 GenericHost 和 依賴注入實現的一個擴展庫,可讓你更方便更容易的在測試項目裏實現依賴注入,並且我以爲另一點很好的是能夠更好的控制操做流程,好比不少在啓動測試以前去作的初始化操做,更好用的流程控制。git
最近把咱們公司的測試項目大多基於 Xunit.DependencyInjection
改造了,使用效果很好。github
最近把個人測試項目從原來本身手動啓動一個 Web Host 改爲了基於 Xunit.DepdencyInjection
來使用,同時也是爲咱們公司的一個項目的集成測試的更新作準備,用起來很香~app
我以爲 Xunit.DependencyInjection
解決了我兩個很大的痛點,一個是依賴注入的代碼寫起來不爽,一個是更簡單的流程控制處理,下面大概介紹一下asp.net
XUnit.DependencyInjection
工做流程Xunit.DepdencyInjection
主要的流程在 DependencyInjectionTestFramework 中,詳見 https://github.com/pengweiqhca/Xunit.DependencyInjection/blob/7.0/Xunit.DependencyInjection/DependencyInjectionTestFramework.cside
首先會去嘗試尋找項目中的 Startup
,這個 Startup
很相似於 asp.net core 中的 Startup
,幾乎徹底同樣,只是有一點不一樣, Startup
不支持依賴注入,不能像 asp.net core 中那樣注入一個 IConfiguration
對象來獲取配置,除此以外,和 asp.net core 的 Startup
有着同樣的體驗,若是找不到這樣的 Startup
就會認爲沒有須要依賴注入的服務和特殊的配置,直接使用 Xunit
原有的 XunitTestFrameworkExecutor
,若是找到了 Startup
就從 Startup
約定的方法中配置 Host
,註冊服務以及初始化配置流程,最後使用 DependencyInjectionTestFrameworkExecutor
執行咱們的 test case.單元測試
源碼解析測試
源碼使用了 C#8 的一些新語法,代碼十分簡潔,下面代碼使用了可空引用類型:ui
DependencyInjectionTestFramework
源碼.net
public sealed class DependencyInjectionTestFramework : XunitTestFramework { public DependencyInjectionTestFramework(IMessageSink messageSink) : base(messageSink) { } protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName) { IHost? host = null; try { // 獲取 Startup 實例 var startup = StartupLoader.CreateStartup(StartupLoader.GetStartupType(assemblyName)); if (startup == null) return new XunitTestFrameworkExecutor(assemblyName, SourceInformationProvider, DiagnosticMessageSink); // 建立 HostBuilder var hostBuilder = StartupLoader.CreateHostBuilder(startup, assemblyName) ?? new HostBuilder().ConfigureHostConfiguration(builder => builder.AddInMemoryCollection(new Dictionary<string, string> { { HostDefaults.ApplicationKey, assemblyName.Name } })); // 調用 Startup 中的 ConfigureHost 方法配置 Host StartupLoader.ConfigureHost(hostBuilder, startup); // 調用 Startup 中的 ConfigureServices 方法註冊服務 StartupLoader.ConfigureServices(hostBuilder, startup); // 註冊默認服務,構建 Host host = hostBuilder.ConfigureServices(services => services .AddSingleton(DiagnosticMessageSink) .TryAddSingleton<ITestOutputHelperAccessor, TestOutputHelperAccessor>()) .Build(); // 調用 Startup 中的 Configure 方法來初始化 StartupLoader.Configure(host.Services, startup); // 返回 testcase executor,準備開始跑測試用例 return new DependencyInjectionTestFrameworkExecutor(host, null, assemblyName, SourceInformationProvider, DiagnosticMessageSink); } catch (Exception e) { return new DependencyInjectionTestFrameworkExecutor(host, e, assemblyName, SourceInformationProvider, DiagnosticMessageSink); } } }
StarpupLoader
源碼3d
public static Type? GetStartupType(AssemblyName assemblyName) { var assembly = Assembly.Load(assemblyName); var attr = assembly.GetCustomAttribute<StartupTypeAttribute>(); if (attr == null) return assembly.GetType($"{assemblyName.Name}.Startup"); if (attr.AssemblyName != null) assembly = Assembly.Load(attr.AssemblyName); return assembly.GetType(attr.TypeName) ?? throw new InvalidOperationException($"Can't load type {attr.TypeName} in '{assembly.FullName}'"); } public static object? CreateStartup(Type? startupType) { if (startupType == null) return null; var ctors = startupType.GetConstructors(); if (ctors.Length != 1 || ctors[0].GetParameters().Length != 0) throw new InvalidOperationException($"'{startupType.FullName}' must have a single public constructor and the constructor without parameters."); return Activator.CreateInstance(startupType); } public static IHostBuilder? CreateHostBuilder(object startup, AssemblyName assemblyName) { var method = FindMethod(startup.GetType(), nameof(CreateHostBuilder), typeof(IHostBuilder)); if (method == null) return null; var parameters = method.GetParameters(); if (parameters.Length == 0) return (IHostBuilder)method.Invoke(startup, Array.Empty<object>()); if (parameters.Length > 1 || parameters[0].ParameterType != typeof(AssemblyName)) throw new InvalidOperationException($"The '{method.Name}' method of startup type '{startup.GetType().FullName}' must without parameters or have the single 'AssemblyName' parameter."); return (IHostBuilder)method.Invoke(startup, new object[] { assemblyName }); } public static void ConfigureHost(IHostBuilder builder, object startup) { var method = FindMethod(startup.GetType(), nameof(ConfigureHost)); if (method == null) return; var parameters = method.GetParameters(); if (parameters.Length != 1 || parameters[0].ParameterType != typeof(IHostBuilder)) throw new InvalidOperationException($"The '{method.Name}' method of startup type '{startup.GetType().FullName}' must have the single 'IHostBuilder' parameter."); method.Invoke(startup, new object[] { builder }); } public static void ConfigureServices(IHostBuilder builder, object startup) { var method = FindMethod(startup.GetType(), nameof(ConfigureServices)); if (method == null) return; var parameters = method.GetParameters(); builder.ConfigureServices(parameters.Length switch { 1 when parameters[0].ParameterType == typeof(IServiceCollection) => (context, services) => method.Invoke(startup, new object[] { services }), 2 when parameters[0].ParameterType == typeof(IServiceCollection) && parameters[1].ParameterType == typeof(HostBuilderContext) => (context, services) => method.Invoke(startup, new object[] { services, context }), 2 when parameters[1].ParameterType == typeof(IServiceCollection) && parameters[0].ParameterType == typeof(HostBuilderContext) => (context, services) => method.Invoke(startup, new object[] { context, services }), _ => throw new InvalidOperationException($"The '{method.Name}' method in the type '{startup.GetType().FullName}' must have a 'IServiceCollection' parameter and optional 'HostBuilderContext' parameter.") }); } public static void Configure(IServiceProvider provider, object startup) { var method = FindMethod(startup.GetType(), nameof(Configure)); method?.Invoke(startup, method.GetParameters().Select(p => provider.GetService(p.ParameterType)).ToArray()); }
來看咱們項目裏的一個單元測試的一個改造,改造以前是這樣的:
這個測試項目使用了老版本的 AutoMapper
,每一個有使用到 AutoMapper
的地方都會須要在測試用例裏調用一下注冊 AutoMapper
mapping 關係的方法來註冊 mapping 關係,由於 Register
方法裏直接調用的Mapper.Initialize
方法註冊 mapping 關係,屢次調用的話會拋出異常,因此每一個測試用例方法裏用到 AutoMapper
的都有這個一段噁心的邏輯
第一次修改,我在 Register
方法作一個簡單的改造,把 try...catch
移除掉了:
可是這樣仍是很不爽,每一個用到 AutoMapper
的測試用例仍是須要調用一下 Register
方法
使用 Xunit.DepdencyInjection
以後就能夠只在 Startup
中的 Configure
方法裏註冊一下就能夠,只須要調用一次就能夠了
後面咱們把 AutoMapper
升級了,使用依賴注入模式使用 AutoMapper
,改造以後的使用
直接在測試用例的類中注入須要的服務 IMapper
便可
集成測試也是相似的,集成測試我用本身的項目做爲一個示例
個人集成測試項目最初是用 xunit
裏的 CollectionFixture
結合 WebHost
來實現的(從 2.2 更新過來的,),在 .net core 3.1 裏能夠直接配置 WebHostedService
就能夠了,而 Xunit.DependencyInjection
是基於 微軟的 GenericHost
的因此,也會比較簡單的作集成。
在 Startup
裏 經過 ConfigureHost
方法配置 IHostBuilder
的擴展方法 ConfigureWebHost
,註冊測試須要的服務,在測試示例類的構造方法中注入服務便可
集成測試改造變動能夠參考: https://github.com/OpenReservation/ReservationServer/commit/d30e35116da0b8d4bf3e65f0a1dcabcad8fecae0
CreateHostBuilder
public class Startup { public IHostBuilder CreateHostBuilder([AssemblyName assemblyName]) { } }
使用這個方法來自定義 IHostBuilder
的時候能夠用這個方法,一般可能不太會用到這個方法,能夠經過 ConfigureHost
方法來配置 Host
默認是直接 new HostBuilder()
, 想要構建 aspnet.core 裏默認配置的 HostBuilder
, 可使用 Host.CreateDefaultBuilder()
來建立 IHostBuilder
ConfigureHost
配置 Host
public class Startup { public void ConfigureHost(IHostBuilder hostBuilder) { } }
經過 ConfigureHost
來配置 Host
,能夠經過這個方法配置 IConfiguration
,也能夠配置要註冊的服務等
配置能夠經過 IHostBuilder
的擴展方法 ConfigureAppConfiguration
來更新配置
ConfigureServices
public class Startup { public void ConfigureServices(IServiceCollection services[, HostBuilderContext context]) { } }
若是不須要讀取 IConfiguration
能夠經過直接使用 ConfigurationServices(IServiceCollection services)
方法
若是須要讀取 IConfiguration
,能夠經過 ConfigureServices(IServiceCollection services, HostBuilderContext context)
方法經過 HostBuilderContext.Configuration
來訪問配置對象 IConfiguration
Configure
public class Startup { public void Configure([IServiceProvider applicationServices]) { } }
Configure
方法能夠沒有參數,也支持全部注入的服務,和 asp.net core 裏的 Configure
方法相似,一般能夠在這個方法裏作一些初始化配置
若是你有在使用 Xunit
的時候遇到上述問題,推薦你試一下 Xunit.DependenceInjection
這個項目,十分值得一試~~