使用 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);
// 创建 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
,注册测试需要的服务,在测试示例类的构造方法中注入服务即可
Startup 支持的方法
CreateHostBuilder
public class Startup
{
public IHostBuilder CreateHostBuilder([AssemblyName assemblyName]) { }
}
使用这个方法来自定义 IHostBuilder
的时候可以用这个方法,通常我们可能不会用到这个方法,可以通过 ConfigureHost
方法来配置 Host
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 改造测试项目的更多相关文章
- 在 xunit 测试项目中使用依赖注入
在 xunit 测试项目中使用依赖注入 Intro 之前写过几篇 xunit 依赖注入的文章,今天这篇文章将结合我在 .NET Conf 上的分享,更加系统的分享一下在测试中的应用案例. 之所以想分享 ...
- 关于iOS10 Xcode8真机测试项目出现的问题 "code signing is required for product type 'xxxxx' in SDK 'iOS 10.0"..
昨天用真机测试项目出现这样的错误,在网上搜集了一些信息,所以将自己的经验分享出来帮助更多的人. 第一步: 检查你的1和2是否填写正确,如果你是运行别人的项目,BundleIdentifier要和你的X ...
- 编码的UI测试项目——Visual Studio 2013
今天实现了一次编码的UI测试项目,以下是我进行测试的过程: 1.新建测试项目 在visual studio中(我用的版本是2013 update2)点击文件->新建->项目,选择“编码的U ...
- 02.基于IDEA+Spring+Maven搭建测试项目--详细过程
一.背景介绍 1.1公司相关技术 Git:是一款免费的开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目,方便多人集成开发 Maven:是基于项目对象模型(POM),可以通过一小段描述信息 ...
- 01.基于IDEA+Spring+Maven搭建测试项目--综述
目前公司的测试工作中常见两种接口:HTTP和Dubbo,这两种接口类型均可以使用相关测试工具进行测试,但都会有一定的局限性和不便之处,具体如下: 1.HTTP接口,当需要对于参数进行加密解密时,就得对 ...
- Android开发2——创建测试项目
一.创建普通Android项目 二.在AndroidManifest.xml添加两个配置 <?xml version="1.0" encoding="utf-8 ...
- 在同一服务器使用git分支建立线上 和 测试 项目
分支分配文件夹后 sourcetree 创建分支与合并 https://blog.csdn.net/qq_34975710/article/details/74469068 线上分支master 测试 ...
- 基于Ubuntu Server 16.04 LTS版本安装和部署Django之(五):测试项目
基于Ubuntu Server 16.04 LTS版本安装和部署Django之(一):安装Python3-pip和Django 基于Ubuntu Server 16.04 LTS版本安装和部署Djan ...
- Robotium实践之路基于APK创建测试项目
1.重新对包进行签名操作 .启动re-sign.jar文件 .找到相应的APK,拖拽置resigner中 2.创建基于APK测试的测试工程 .新建一个安卓测试项目 .选择this project
随机推荐
- centos7修改ssh端口及添加ssh监听端口
ssh 修改默认端口 [root@node-1 ~]# vi /etc/ssh/sshd_config 修改port 为 5522 重启[root@node-1 ~]# systemctl resta ...
- 【字符串算法】字典树(Trie树)
什么是字典树 基本概念 字典树,又称为单词查找树或Tire树,是一种树形结构,它是一种哈希树的变种,用于存储字符串及其相关信息. 基本性质 1.根节点不包含字符,除根节点外的每一个子节点都包含一个字符 ...
- PropertySheet外壳扩展AppWizard
下载source files - 39 Kb 下载Wizard - 17 Kb 本文旨在简化属性表外壳扩展的实现.它紧接我的第一篇文章 处理上下文菜单壳扩展和灵感 由Michael Dunn最优秀的系 ...
- TP5调用小程序微信支付,回调,在待支付中再次调用微信支付
1,必须要有 $mch_id $key $appid这三个值,是需要去申请的,我是直接用公司的2,购买商品订单号用户openid统一下单名称商品价格(必须以分为单位,调起微信支付)服务器的ip地址(没 ...
- Python库之SQLAlchemy
一.SQLAlchemy简介 1.1.SQLAlchemy是什么? sqlalchemy是一个python语言实现的的针对关系型数据库的orm库.可用于连接大多数常见的数据库,比如Postges.My ...
- LVS+keepalive
LVS+keepalive 什么是keepalive Keepalived是Linux下一个轻量级别的高可用解决方案.高可用(High Avalilability,HA),其实两种不同的含义:广义来讲 ...
- JAVA基础 随机点名器案例
1.1 案例介绍 随机点名器,即在全班同学中随机的找出一名同学,打印这名同学的个人信息. 此案例在我们昨天课程学习中,已经介绍,现在我们要做的是对原有的案例进行升级,使用新的技术来实现. 我 ...
- 自定义chrome新标签页
[跳转GitHub] chromeNewTab 自定义chrome新标签页.由于不想发布到chrome应用商店,因此搜了一下不用开发者模式就能用的方法. 使用说明 下载chrome的一个[window ...
- Jmeter之参数化函数助手__randomstring
1.Tools->函数助手对话框,选择__Random String,2表示随机生成的字符长度:3表示从哪些字符中随机生成:然后点击生成,得到对应的变量: 5中372表示该函数随机生成的字符串, ...
- 干货分享:用一百行代码做一个C/C++表白小程序,程序员的浪漫!
前言:很多时候,当别人听到你是程序员的时候.第一印象就是,格子衫.不浪漫.直男.但是程序员一旦浪漫起来,真的没其他人什么事了.什么纪念日,生日,情人节,礼物怎么送? 做一个浪漫的程序给她,放上你们照片 ...