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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/307027.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

discuz mysql data_Discuz!显示 Database Error的原因和解决方法

今天打开Discuz搭建的论坛显示&#xff1a;原因一&#xff1a;数据库表太大比如mysql数据库的表内容太大&#xff0c;超过10G就有可能会影响discuz论坛的运行。Discuz! Database Error是什么原因&#xff0c;怎么修复这种情况可以通过对数据库分表的方法来解决。原因二&#xf…

项目开发中经常有一些被嫌弃的小数据,现在全丢给 FastDFS

在我们开发项目的时候&#xff0c;经常会遇到大块数据的问题&#xff08;2M-100M&#xff09;&#xff0c;比如说保存报表中1w个人的ID号&#xff0c;说实话&#xff0c;这些数据存储在服务器哪里都被嫌弃&#xff0c;放在redis&#xff0c;mongodb中吧&#xff0c;一下子你就会…

java 反射 int_Java 反射由浅入深 | 进阶必备

原标题&#xff1a;Java 反射由浅入深 | 进阶必备一、Java 反射机制参考了许多博文&#xff0c;总结了以下个人观点&#xff0c;若有不妥还望指正&#xff1a;Java 反射机制在程序运行时&#xff0c;对于任意一个类&#xff0c;都能够知道这个类的所有属性和方法&#xff1b;对…

寻找性能更优秀的不可变小字典

Dictionary 是一个很常用的键值对管理数据结构。但是在性能要求严苛的情况下&#xff0c;字典的查找速度并不高。所以&#xff0c;我们需要更快的方案。需求说明 这里&#xff0c;我们需要一个 PropertyInfo 和委托对应的映射关系&#xff0c;这样我们就可以存储《寻找性能更优…

一款基于.NET Core的认证授权解决方案-葫芦藤1.0开源啦

背景18年公司准备在技术上进行转型&#xff0c;而公司技术团队是互相独立的&#xff0c;新技术的推动阻力很大。我们需要找到一个切入点。公司的项目很多&#xff0c;而各个系统之间又不互通&#xff0c;导致每套系统都有一套登录体系&#xff0c;给员工和客户都带来极大的不便…

.NET架构小技巧(8)——优待异常

天有不测风云&#xff0c;人有旦夕祸福&#xff0c;程序呢——会有异常错误。C#中用try&#xff0c;catch&#xff0c;finally来捕捉处理异常&#xff0c;捕捉谁的异常呢&#xff1f;一般都是系统类库或三方类库中抛出的异常&#xff0c;那如果我自己架构程序&#xff0c;异常也…

跟我一起学.NetCore之EF Core 实战入门,一看就会

前言还记得当初学习数据库操作时&#xff0c;用ADO.NET一步一步地进行数据操作及查询&#xff0c;对于查询到的数据还得对其进行解析&#xff0c;然后封装返回给应用层&#xff1b;遇到这种重复而繁琐的工作&#xff0c;总有一些大神或团队对其进行封装&#xff0c;从而出现了很…

java 声明变量构成_Java—变量

1.1 按数据类型分类1.1.1 基本数据类型(四类八种)☛ 引用数据类型的特点存的是地址值,可以为null值☛ 基本数据类型的特点存的是具体的值,不可以是null值☛ 整型整型取值范围字节数byte(字节)-128 ~ 1271byteshort(短整型)-2byteint(默认整型)-4bytelong(长整型)12345678L8byte…

寻找性能更优秀的动态 Getter 和 Setter 方案

反射获取 PropertyInfo 可以对对象的属性值进行读取或者写入&#xff0c;但是这样性能不好。所以&#xff0c;我们需要更快的方案。方案说明 就是用表达式编译一个Action<TObj,TValue>作为 Setter&#xff0c;编译一个Func<TObj,TValue>作为 Getter。然后把这些编译…

Newbe.ObjectVisitor 0.2.10 发布,更花里胡哨

更新内容 现在&#xff0c;你可以通过上下文修改属性的值了&#xff1a;//✔️ from 0.2 // 可以修改属性 o.V().ForEach((context) > ModifyData(context)).Run();public static void ModifyData(IObjectVisitorContext<Yueluo,string> context) {context.Value con…

.NET 5 和 C#9 /F#5 一起到来, 向实现 .NET 统一迈出了一大步

经过一年多的开发&#xff0c;Microsoft 于北京时间 11 月 11 日&#xff08;星期三&#xff09;发布了其 .NET 5软件开发平台&#xff0c;强调平台的统一&#xff0c;并引入了 C# 9 和 F# 5 编程语言&#xff0c;新平台朝着桌面、Web、移动、云和 IoT 目标统一 .NET 开发体验的…

.NetCore HttpClient发送请求的时候为什么自动带上了一个RequestId头部?

奇怪的问题最近在公司有个系统需要调用第三方的一个webservice。本来调用一个下很简单的事情&#xff0c;使用HttpClient构造一个SOAP请求发送出去拿到XML解析就是了。可奇怪的是我们的请求在运行一段时间后就会被服务器504给拒绝掉了。导致系统无法使用&#xff0c;用户叫苦连…

ASP.NETCore小技巧:使用测试用户中间件

哈喽大家好&#xff0c;这篇文章其实很早就想写了&#xff0c;因为一直会有小伙伴问到&#xff0c;但是我却始终拿不到好的方案&#xff0c;最近在录制《eShopOnContainer微服务架构》的视频&#xff0c;碰巧就看到了微软官方的代码中也有这方面的需求&#xff0c;而且和我的需…

【招聘(深圳)】华强方特文化科技集团 .NET工程师

.NET高级开发工程师&#xff08;18-25K&#xff09;岗位职责&#xff1a;负责系统需求分析与设计&#xff1b;根据业务确定实现方案&#xff1b;对现有系统缺陷提出优化方案&#xff1b;负责系统关键功能开发及维护&#xff0c;保障系统的正常运行&#xff1b;带领指导团队开发…

11座城市,58个.NET最新岗位速览,内推直通面试官!

十一月风雪客&#xff0c;十二月乘衣归&#xff01;各个大厂秋招进行时&#xff0c;你行动了吗&#xff1f;借着这阵风&#xff0c;今天为大家提供一批.NET开发岗位内推&#xff01;58个优质的.NET开发岗位年薪过万到百万不等&#xff0c;总有一个适合你&#xff01;包含全国各…

pdo mysql_PDO MySQL

PDO MySQL如果文章有成千上万篇&#xff0c;该怎样保存&#xff1f;数据保存有多种方式&#xff0c;比如单机文件、单机数据库(SQLite)、网络数据库(MySQL、MariaDB)等等。根据项目来选择&#xff0c;做Web一般采用MySQL&#xff0c;本书也以MySQL为例。自学&#xff1a;1天。假…

C# 8: 可变结构体中的只读实例成员

在之前的文章中我们介绍了 C# 中的 只读结构体&#xff08;readonly struct&#xff09;[1] 和与其紧密相关的 in 参数[2]。今天我们来讨论一下从 C# 8 开始引入的一个特性&#xff1a;可变结构体中的只读实例成员&#xff08;当结构体可变时&#xff0c;将不会改变结构体状态的…

部署Dotnet Core应用到Kubernetes(一)

最近闲了点&#xff0c;写个大活&#xff1a;部署Dotnet应用到K8s。写在前边的话一直想完成这个主题。但这个主题实在太大了&#xff0c;各种拖延症的小宇宙不时爆发一下&#xff0c;结果就拖到了现在。这个主题&#xff0c;会是一个系列。在这个系列中&#xff0c;我会讨论将应…

JAVA实验报告九异常处理_Java课后练习9(异常处理)

动手动脑1:import javax.swing.*;class AboutException {public static void main(String[] a){int i1, j0, k;ki/j;try{k i/j; // Causes division-by-zero exception//throw new Exception("Hello.Exception!");}catch ( ArithmeticException e){System.out.print…

java 实现 指派_TAP任务指派问题的汇编实现

近六周的课程设计&#xff0c;编了一个四百行的汇编程序&#xff0c;编的过程很不顺利&#xff0c;遇到种种意想不到的困难&#xff0c;但最终能够实现&#xff0c;可谓欣喜若狂&#xff0c;这期间学到了好多好多&#xff0c;遇到问题怎么精下心来解决&#xff0c;同时对汇编的…