使用 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) &&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

  • 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 改造测试项目相关推荐

  1. 使用Specflow 和XUnit 进行BDD测试项目配置方法

    开发环境:Vistual Studio 2010,项目类型 asp.net MVC 3 工具: SpecFlow: 下载地址:https://github.com/techtalk/SpecFlow/ ...

  2. xUnit测试项目使用笔记

    一.新建测试项目 xUnit 二.新建一个基础的测试基类:BaseTest,测试类基类: 初始化程序默认需要验证内容或数据库.redis等内容和公用的写日志方式 /// <summary> ...

  3. 行车记录仪软件测试报告工作表,车载终端设备行驶记录仪794-2019检测报告测试项目...

    JT∕T 道路运输车辆卫星定位系统车载终端技术要求 标准解读 随着我国城市化脚步的加快,车已经成为了人们生活水平提高的象征,不过也给我们带来了很多问题,交通事故频发而导致的人员伤亡和财产损失越来越得到 ...

  4. 在线机房改造类项目建设难点的研究

    摘要 随着IT技术的不断发展,IT设备的运行环境要求越来越高,更新换代的速率也越来越快.其中作为IT系统运行的载体--数据中心,需要持续适应不断提升的运行环境要求. 由于数据中心作为一个建筑,生命周期 ...

  5. 使用命令行运行 jMeter 测试项目

    jMeter 不建议使用 GUI 模式运行性能测试. GUI 模式仅适用于创建测试项目或者调试. 命令行: jmeter -n -t jerrysandbox.jmx -l 11.txt 其中 -n ...

  6. linux如何执行平台,如何在Linux平台运行HelloWorld及测试项目

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 Cocos2d-x引擎自Cocos2d-1.0.1-x-0.9.2版本以来支持Linux平台. 本文介绍如何在Linux及Android模拟器中运行Hel ...

  7. 使用C#为MSTest测试项目实现自定义断言

    前言 MSTest测试项目为我们实现了断言类Assert,用于报告代码行为的正确性,比如: var result = Calculator.Add(1,2); Assert.AreEqual(3, r ...

  8. Incapsula企业版测试项目

    2019独角兽企业重金招聘Python工程师标准>>> 在网站规模安全及性能上, Incapsula公司能够满足最大组织的需求 将多个设备和应用程序整合到单个基于云的服务中,简化您的 ...

  9. 测试项目开源_测验您对开源的承诺

    测试项目开源 在今年的OSCON上, Bluehost的 Jared Smith谈到了我们的公司如何成为良好的开源公民. 在ByWater Solutions ,我的工作包括参与社区外展活动,并让每个 ...

最新文章

  1. 解决dell poweredge 2850 服务器系统内存限制
  2. 去某大厂三面总监面,因为迟到了5分钟,面试官当着我的面把简历扔垃圾桶了...
  3. couchdb 垂直权限绕过漏洞(cve-2017-12635)
  4. matlab求adc信号的信噪比,关于ADC的信噪比 - pengyouxiaohui的日志 - EETOP 创芯网论坛 (原名:电子顶级开发网) -...
  5. mysql创建函数1418_Mysql中创建函数报“ERROR 1418 ”的解决方法
  6. java中 int 比较_java中Integer与int的种种比较你知道多少?
  7. 决策树的选择,哪个放在第一个需要决策的环节
  8. 原来在首席架构眼里MySQL果然如此不一样!
  9. R语言决策树:NBA球员如何拿到大合同
  10. 现在有哪些好用的程序员学习交流的网站或者app?
  11. 【手把手带你学JavaSE】第八篇:抽象类和接口
  12. C语言实现lagrange theorem拉格朗日定理的算法(附完整源码)
  13. 蚁群优化算法(ACO)
  14. 迷你世界进云服务器需要密码,迷你世界云服务器
  15. verilog实现I2C控制器 (小梅哥思路)----详细解析
  16. [SSD固态硬盘技术 17] 缓存(DRAM)对性能的影响机制
  17. 【生活类】洗衣机不排水怎么解决?
  18. Python监听鼠标左键被点击,gogo,急停,
  19. 皮影机器人ppt_皮影演绎机器人
  20. 二层广播(帧广播)和三层广播(路由器广播)有什么区别?

热门文章

  1. 粒子系统(一):从零开始画一颗树
  2. 用POP动画引擎实现弹簧动画(POPSpringAnimation)
  3. 从无到有到完善 - Teams抽奖机器人开发历程
  4. 如何在PowerPoint中制作打字机或命令行动画
  5. 《Apache Kafka实战》读书笔记-调优Kafka集群
  6. 个人总结的一个中高级Java开发工程师或架构师需要掌握的一些技能...
  7. 如何在一小时内更新100篇文章?-Evernote Sync插件介绍
  8. OutLook2016修改注册表迁移.ost文件数据
  9. 重要的ui组件——Behavior
  10. CSS Id 和 Class