使用 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);            // 创建 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 源码

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

Startup 支持的方法

  • 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 方法类似,通常可以在这个方法里做一些初始化配置

More

如果你有在使用 Xunit 的时候遇到上述问题,推荐你试一下 Xunit.DependenceInjection 这个项目,十分值得一试~~

Reference

(0)

相关推荐

  • 设计模式中的那些工厂

    Intro 设计模式中有几个工厂模式,聊一聊这几个工厂模式的各自用法和使用示例,工厂模式包含简单工厂,抽象工厂,工厂方法,这些均属于创建型模式, 所谓创建型模式,就是说这几个设计模式是用来创建对象的. ...

  • 优化委托的 `DynamicInvoke`

    Intro 委托方法里有一个 DynamicInvoke 的方法,可以在不清楚委托实际类型的情况下执行委托方法,但是用 DynamicInvoke 去执行的话会比直接用 Invoke 的方法会慢上很多 ...

  • 基于.NetCore3.1系列 ——认证授权方案之Swagger加锁

    一.前言 在之前的使用Swagger做Api文档中,我们已经使用Swagger进行开发接口文档,以及更加方便的使用.这一转换,让更多的接口可以以通俗易懂的方式展现给开发人员.而在后续的内容中,为了对a ...

  • Asp.Net Core 3.1学习- 应用程序的启动过程(5)

    前言 本文主要讲的是Asp.Net Core的启动过程,帮助大家掌握应用程序的关键配置点. 1.创建项目 1.1.用Visual Studio 2019 创建WebApi项目. 这里面可以看到有两个关 ...

  • ASP.NET Core笔记(1) - 了解Startup类

    Startup构造函数 ConfigureServices方法 Configure方法 在ConfigureWebHostDefaults中直接配置服务和请求管道 ASP.NET Core一般使用St ...

  • 基于.NetCore3.1系列 —— 使用Swagger做Api文档 (下篇)

    前言 回顾上一篇文章<使用Swagger做Api文档 >,文中介绍了在.net core 3.1中,利用Swagger轻量级框架,如何引入程序包,配置服务,注册中间件,一步一步的实现,最终 ...

  • TDR测试项目解说

    早期测试高频参数的仪器主要是TDR和NA,首先我们对于测试设备的测试差异做个简要普及: 你要弄懂TDR,NA之间的差别; TDR发射固定频率信号(低频,一般200KHz)去侦测待测物在微小长度分段下的 ...

  • 【计划重启】!高速磁浮,中企与德国磁悬浮测试项目

    简介 2021年3月,当地媒体报道,中国轨道行业公司希望使用德国Transrapid位于埃姆斯兰县的测试轨道.测试轨道所在的机构负责人对此表示肯定,并关于"中企是否可利用相关轨道进行测试&q ...

  • 电池UL2054安全标准与测试项目介绍

    电池上亚马逊是需要UL2054报告的:下面我们来解读一下电池UL2054安全标准: 什么是UL2054标准? UL2054标准是电池包的相关标准,范围是针对拥有两个或以上的电池以串联或者并联的方式结合 ...

  • 儿童玩具学步车EN71测试项目有哪些?

    玩具.儿童用品和礼品主要面对的是特殊消费人群---儿童,各个国家都对此类产品制定出相当严格的国家标准及地区标准!凭借专业的技术人才及实验室,对产品进行测试,认证及咨询服务,针对不同的产品类型.出口国家 ...

  • 蓝牙产品FCC认证的测试项目有哪些?

    带蓝牙模块的手机,电脑,及其他场合应用中的蓝牙技术,如蓝牙耳机,蓝牙音箱,蓝牙鼠标,蓝牙键盘等,在申请FCC ID认证时,需遵循FCC PART15的法令要求. 蓝牙的频段通常采跳频技术(FHSS), ...

  • 脱发、焦虑,一个旧厂改造的项目难倒了整个设计院?

    你知道旧厂改造为什么受欢迎吗? 因为有国家政策支持呀! <加强城市规划建设管理工作若干意见>中 指出要通过维护加固老建筑 改造旧厂房等措施 恢复老城区功能和活力 不过你知道旧厂房要怎么改造 ...

  • IEC61960测试项目

    IEC61960是针对便携式碱性或非酸性电解液二次锂电芯和电池的性能测试标准. IEC61960测试项目:蓄电量恢复量.蓄电量保持量.终电压,放电终止电压.标称电压.额定容量.锂二次电池.锂二次电池单 ...

  • 手机充电器CE认证测试项目和要求

    手机充电器是为手机电池充电的一个电源,主要是稳压电源.提供稳定工作电压和足够的电流)加上必要的恒流.限压.限时等控制电路构成.手机充电器是一类非常常见的电子产品,出口欧盟国家需要做CE认证. 充电器C ...

  • 指尖陀螺ASTM F963测试流程及测试项目

    主要内容分三部分: 1.物理机械性能 2.燃烧性能 3.化学性能 物理和机械性能的要求主要涉及到:冲击试验.跌落试验.部件移取的拉力试验.压力试验.挠曲试验等等. 玩具易燃性测试程序按照16CFR 1 ...