使用 Xunit.DependencyInjection 改造测试项目
使用 Xunit.DependencyInjection 改造測試項目
Intro
這篇文章拖了很長時間沒寫,之前也有介紹過 Xunit.DependencyInjection 這個項目,這個項目是由大師寫的一個 Xunit 基于微軟 GenericHost 和 依賴注入實現的一個擴展庫,可以讓你更方便更容易的在測試項目里實現依賴注入,而且我覺得另外一點很好的是可以更好的控制操作流程,比如很多在啟動測試之前去做的初始化操作,更好用的流程控制。
最近把我們公司的測試項目大多基于 Xunit.DependencyInjection 改造了,使用效果很好。
最近把我的測試項目從原來自己手動啟動一個 Web Host 改成了基于 Xunit.DepdencyInjection 來使用,同時也是為我們公司的一個項目的集成測試的更新做準備,用起來很香~
我覺得 Xunit.DependencyInjection 解決了我兩個很大的痛點,一個是依賴注入的代碼寫起來不爽,一個是更簡單的流程控制處理,下面大概介紹一下
XUnit.DependencyInjection 工作流程
Xunit.DepdencyInjection 主要的流程在 DependencyInjectionTestFramework ?中,詳見 https://github.com/pengweiqhca/Xunit.DependencyInjection/blob/7.0/Xunit.DependencyInjection/DependencyInjectionTestFramework.cs
首先會去嘗試尋找項目中的 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 的一些新語法,代碼十分簡潔,下面代碼使用了可空引用類型:
DependencyInjectionTestFramework 源碼
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);//?創建?HostBuildervar?hostBuilder?=?StartupLoader.CreateHostBuilder(startup,?assemblyName)???new?HostBuilder().ConfigureHostConfiguration(builder?=>builder.AddInMemoryCollection(new?Dictionary<string,?string>?{?{?HostDefaults.ApplicationKey,?assemblyName.Name?}?}));//?調用?Startup?中的?ConfigureHost?方法配置?HostStartupLoader.ConfigureHost(hostBuilder,?startup);//?調用?Startup?中的?ConfigureServices?方法注冊服務StartupLoader.ConfigureServices(hostBuilder,?startup);//?注冊默認服務,構建?Hosthost?=?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 源碼
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)?&¶meters[1].ParameterType?==?typeof(HostBuilderContext)?=>(context,?services)?=>?method.Invoke(startup,?new?object[]?{?services,?context?}),2?when?parameters[1].ParameterType?==?typeof(IServiceCollection)?&¶meters[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
Startup 支持的方法
CreateHostBuilder
使用這個方法來自定義 IHostBuilder 的時候可以用這個方法,通常可能不太會用到這個方法,可以通過 ConfigureHost 方法來配置 Host
默認是直接 new HostBuilder(), 想要構建 aspnet.core 里默認配置的 HostBuilder, 可以使用 Host.CreateDefaultBuilder() 來創建 IHostBuilder
ConfigureHost 配置 Host
通過 ConfigureHost 來配置 Host,可以通過這個方法配置 IConfiguration,也可以配置要注冊的服務等
配置可以通過 IHostBuilder 的擴展方法 ConfigureAppConfiguration 來更新配置
ConfigureServices
如果不需要讀取 IConfiguration 可以通過直接使用 ConfigurationServices(IServiceCollection services) 方法
如果需要讀取 IConfiguration,可以通過 ConfigureServices(IServiceCollection services, HostBuilderContext context) 方法通過 HostBuilderContext.Configuration 來訪問配置對象 IConfiguration
Configure
Configure 方法可以沒有參數,也支持所有注入的服務,和 asp.net core 里的 Configure 方法類似,通常可以在這個方法里做一些初始化配置
More
如果你有在使用 Xunit 的時候遇到上述問題,推薦你試一下 Xunit.DependenceInjection 這個項目,十分值得一試~~
Reference
https://github.com/pengweiqhca/Xunit.DependencyInjection
https://github.com/pengweiqhca/Xunit.DependencyInjection/blob/7.0/Xunit.DependencyInjection/DependencyInjectionTestFramework.cs
https://github.com/OpenReservation/ReservationServer
https://github.com/OpenReservation/ReservationServer/commit/d30e35116da0b8d4bf3e65f0a1dcabcad8fecae0
總結
以上是生活随笔為你收集整理的使用 Xunit.DependencyInjection 改造测试项目的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 谈谈.NET Core IService
- 下一篇: 项目开发中经常有一些被嫌弃的小数据,现在