Asp.Net Core 单元测试正确姿势

640?wx_fmt=png背景

ASP.NET Core 支持依赖关系注入 (DI) 软件设计模式,并且默认注入了很多服务,具体可以参考 官方文档, 相信只要使用过依赖注入框架的同学,都会对此有不同深入的理解,在此无需赘言。

然而,在引入 IOC 框架之后,对于之前常规的对于类的依赖(new Class)变成通过构造函数对于接口的依赖(ASP.NET CORE 默认注入方式),这本身更加符合依赖倒置原则,但是对于单元测试来说确会带来另一个问题:由于层层依赖,导致在某个类的方法进行测试的时候,需要构造一大堆该类依赖的接口的实现,非常麻烦。

这个时候,我们脑子里会下意识想一个问题:为什么常用的 .Net 单元测试框架不支持依赖注入?

于是笔者带着这个问题在查阅了一些关于在单元测试中支持依赖注入的讨论Github Issue,以及其他的相关文档,突然明白一个之前一直忽视但实际却非常重要的问题:

在对于一个方法的单元测试中,我们应该关注的是这个方法内部的逻辑测试,而这个方法内部对于外部的依赖,则不在这个单元测试关注的范围内

换言之,单元测试永远都只关注需要测试的方法内部的逻辑实现,至于外部依赖方法的测试,则应该放在另一个专门针对这个方法的单元测试用例中。弄清楚这个问题,我们才能更加理解另一个单元测试不可缺少的框架——Mock框架,在我们写的测试中,应该忽略外部依赖具体的实现,而是通过模拟该接口方法来显示的指定返回值,从而降低该返回值对于当前单元测试结果的影响,而 Mock 框架(例如最常用的Moq),刚好可以满足我们对于接口的模拟需求。

相信有同学跟我有同样的疑惑,并且当我尝试在 ASP.NET CORE 单元测试中的一切外部依赖通过 Mock 的方式进行编写的时候,遇到了一些问题,才有了本篇文章,希望对有同样疑惑的同学有所帮助。

如何对 ASP.NET CORE 常用服务进行单元测试和 Mock

本文以 Xunit 以及 Moq 4.x 为例,展示在常用的 ASP.NET CORE 中会遇到的各种测试情况。

业务服务类示例如下:

public class UserService : IUserService
{private ILogger _logger;private IOptions<RabbitMqOptions> _options;private IConfiguration _configuration;public UserService(ILogger<UserService> logger, IConfiguration configuration, IOptions<RabbitMqOptions> options){this._logger = logger;this._options = options;this._configuration = configuration;}public void Login(){var hostName = this._configuration["RabbitMqOptions:Host"];var options = this._options.Value;//do somethingthis._logger.Log(LogLevel.Information, new EventId(), "Login", null, (m, e) => m);}public string GetUserInfo(){return $"hello world!";}
}public class RabbitMqOptions
{public string Host { get; set; }public string UserName { get; set; }public string Password { get; set; }
}

1. IConfiguration 获取配置Mock

获取单个配置:

var mockConfiguration = new Mock<IConfiguration>();
mockConfiguration.SetupGet(_ => _["RabbitMqOptions:Host"]).Returns("127.0.0.1");

Mock IOptions<T>

var mockRabbitmqOptions = new Mock<IOptions<RabbitMqOptions>>();
mockRabbitmqOptions.Setup(_ => _.Value).Returns(new RabbitMqOptions
{Host = "127.0.0.1",UserName = "root",Password = "123456"
});

2. Mock 方法返回参数

[Fact]
public void mock_return_test()
{var mockInfo = "mock hello world";var mockUserService = new Mock<IUserService>();mockUserService.Setup(_ => _.GetUserInfo()).Returns(mockInfo);var userInfo= mockUserService.Object.GetUserInfo();Assert.Equal(mockInfo, userInfo);
}

3. ILogger 日志组件 Mock

通过 logger.Verify 验证日志至少输出一次:

[Fact]
public void log_in_login_test()
{var logger = new Mock<ILogger<UserService>>();var userService = new UserService(logger.Object);userService.Login();logger.Verify(_ => _.Log(It.IsAny<LogLevel>(),It.IsAny<EventId>(),It.IsAny<string>(),It.IsAny<Exception>(),It.IsAny<Func<string, Exception, string>>()),Times.Once);
}

4. ServiceCollection 单元测试

public static void AddUserService(this IServiceCollection services, IConfiguration configuration)
{services.TryAddSingleton<IUserService, UserService>();
}
 [Fact]
public void add_user_service_test()
{var mockConfiguration = new Mock<IConfiguration>();var serviceConllection = new ServiceCollection();serviceConllection.AddUserService(mockConfiguration.Object);var provider = serviceConllection.BuildServiceProvider();var userService = provider.GetRequiredService<IUserService>();Assert.NotNull(userService);
}

5. Middleware 单元测试

Middleware单元测试重点在于对委托 _next 的模拟

public class HealthMiddleware
{private readonly RequestDelegate _next;private readonly ILogger _logger;private readonly string _healthPath = "/health";public HealthMiddleware(RequestDelegate next, ILogger<HealthMiddleware> logger, IConfiguration configuration){this._next = next;this._logger = logger;var healthPath = configuration["Consul:HealthPath"];if (!string.IsNullOrEmpty(healthPath)){this._healthPath = healthPath;}}public async Task Invoke(HttpContext httpContext){if (httpContext.Request.Path == this._healthPath){httpContext.Response.StatusCode = (int)HttpStatusCode.OK;await httpContext.Response.WriteAsync("I'm OK!");}elseawait _next(httpContext);}
}

单元测试:

public class HealthMiddlewareTest
{private readonly Mock<ILogger<HealthMiddleware>> _mockLogger;private readonly Mock<IConfiguration> _mockConfiguration;private readonly string _healthPath = "/health";private readonly HttpContext _httpContext;private readonly Mock<RequestDelegate> _mockNext; //middleware nextpublic HealthMiddlewareTest(){this._mockConfiguration = new Mock<IConfiguration>();this._mockConfiguration.SetupGet(c => c["Consul:HealthPath"]).Returns(_healthPath);this._mockLogger = new Mock<ILogger<HealthMiddleware>>();this._mockLogger.Setup(_ => _.Log<object>(It.IsAny<LogLevel>(), It.IsAny<EventId>(),It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>())).Callback<LogLevel, EventId, object, Exception, Func<object, Exception, string>>((logLevel, eventId, message, ex, fun) =>{Console.WriteLine($"{logLevel}\n{eventId}\n{message}\n{message}");});this._httpContext = new DefaultHttpContext();this._httpContext.Response.Body = new MemoryStream();this._httpContext.Request.Path = this._healthPath;this._mockNext = new Mock<RequestDelegate>();//next 委托 Mockthis._mockNext.Setup(_ => _(It.IsAny<HttpContext>())).Returns(async () =>{await this._httpContext.Response.WriteAsync("Hello World!"); //模拟http请求最终输出});}[Fact]public async Task health_request_test(){var middleWare = new HealthMiddleware(this._mockNext.Object, this._mockLogger.Object,this._mockConfiguration.Object);await middleWare.Invoke(this._httpContext);//执行middlewarethis._httpContext.Response.Body.Seek(0, SeekOrigin.Begin); //获取监控检查请求获取到的response内容var reader = new StreamReader(this._httpContext.Response.Body);var returnStrs = await reader.ReadToEndAsync();Assert.Equal("I'm OK!", returnStrs);//断言健康检查api是否中间件拦截输出 "I'm OK!"}[Fact]public async Task general_request_test(){this._mockConfiguration.SetupGet(c => c["Consul:HealthPath"]).Returns("/api/values");var middleWare = new HealthMiddleware(this._mockNext.Object, this._mockLogger.Object,this._mockConfiguration.Object);await middleWare.Invoke(this._httpContext);this._httpContext.Response.Body.Seek(0, SeekOrigin.Begin);var reader = new StreamReader(this._httpContext.Response.Body);var returnStrs = await reader.ReadToEndAsync();Assert.Equal("Hello World!", returnStrs); //断言非健康检查请求api返回模拟 Hello World!}
}

6. Mock HttpClient

HttpClient 中的 GetAsync、PostAsync 等方法底层实际都是通过HttpMessageHandler 调用 SendAsync 完成(见源码),所以在 Mock HttpClient 时,实际需要 Mock 的是 HttpMessageHandler 的 SendAsync 方法:

[Fact]
public async Task get_async_test()
{var responseContent = "Hello world!";var mockHttpClient = this.BuildMockHttpClient("https://github.com/", responseContent);var response = await mockHttpClient.GetStringAsync("/api/values");Assert.Equal(responseContent, response);
}private HttpClient BuildMockHttpClient(string baseUrl, string responseStr)
{var mockHttpMessageHandler = new Mock<HttpMessageHandler>();mockHttpMessageHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync",ItExpr.IsAny<HttpRequestMessage>(),ItExpr.IsAny<CancellationToken>()).ReturnsAsync((HttpRequestMessage request, CancellationToken token) =>{HttpResponseMessage response = new HttpResponseMessage();response.Content = new StringContent(responseStr, Encoding.UTF8);return response;});var mockHttpClient = new HttpClient(mockHttpMessageHandler.Object);mockHttpClient.BaseAddress = new Uri(baseUrl);return mockHttpClient;
}

结语

几个问题:

  1. CI/CD 流程中应该包含单元测试

  2. 单元测试覆盖率

  3. 新人问题:为何要写单元测试?

其实编程也如人生三境:看山是山;看山不是山;看山还是山;阶段不同,认知不同,唯有坚持不懈,持之以恒,才能不断进步,提升境界,这不就是人追求的根本么!

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

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

相关文章

程序员与「中台」的爱恨交错

大家好&#xff0c;我是Z哥。这篇文章比较长&#xff0c;有5200字&#xff0c;不过希望你能耐心看完&#xff0c;特别是程序员。中台这个词&#xff0c;最近两年特别火&#xff0c;它的爆发源于2015年张勇在阿里发出的内部信中提到的“大中台&#xff0c;小前台”战略。随后吸引…

ML.NET 1.4 发布,跨平台机器学习框架

ML.NET 是一个面向 .NET 开发人员的开源和跨平台机器学习框架&#xff0c;它包括 Model Builder 和 CLI(命令行接口)&#xff0c;让使用自动机器学习(AutoML)构建自定义机器学习模型变得更容易。1.4 版本已经发布了&#xff0c;以下是本次更新的一些亮点&#xff1a;基于 GPU 支…

使用ASP.NET Core 3.x 构建 RESTful API - 2. 什么是RESTful API

1. 使用ASP.NET Core 3.x 构建 RESTful API - 1.准备工作什么是REST REST一词最早是在2000年&#xff0c;由Roy Fielding在他的博士论文《Architectural Styles and the Design of Network-based Software Architecture》中提出的。他在本文中创造了REST这个术语。这篇论文的地…

Visual Studio Online 的 FAQ:iPad 支持、自托管环境、Azure 账号等

iPad 支持 目前&#xff0c;Web 版 VS Code 只支持基于 Chromium 的浏览器&#xff0c;还不支持 iPad 上的浏览器。但对于 Safari 的支持&#xff0c;是 Visual Studio Online 团队的一件高优先级的任务。更多详情&#xff0c;可以关注&#xff1a; https://github.com/Microso…

2019 .NET China Conf之我逛魔都

趁着参加首届.NET开发者峰会之际&#xff0c;我也是第一次到上海&#xff0c;因此也趁机逛了一下大魔都&#xff0c;和你分享一波我在魔都拍的照片组。酒店所在地&#xff1a;邮电新村地铁站附近为何选择这里&#xff1f;因为离会场酒店6个地铁站&#xff0c;离南京东路和外滩半…

参加首届中国 .NET 开发者峰会有感

参加首届中国 .NET 开发者峰会有感Intro很高兴能够有机会参加首届中国 .NET 开发者峰会&#xff0c;与从全国各地赶来上海参加活动的 .NETer 一起参与这空前的 .NET 的盛会。大会有许多从外地过来参加的开发者们&#xff0c;也有些讲师也是从外地赶过来为我们分享&#xff0c;特…

推荐一款神器-VBAC#代码编辑管理器

网名&#xff1a;liucqa&#xff0c;OFFICE开发领域真大牛&#xff0c;比ExcelHome所有版主和所有出OFFICE开发类书籍的人都要牛的人&#xff0c;出品了它的大作&#xff0c;给大家推荐使用。特色功能&#xff1a;C#&VBA代码格式化/代码收藏/高亮语法详细介绍说明如下&…

【.NET Core 3.0】 46 ║ 授权认证:自定义返回格式

前言哈喽大家好&#xff0c;马上就要年末了&#xff0c;距离新的一年&#xff0c;只有50天了&#xff0c;春节是75天。在这个时节内&#xff0c;天气逐渐变凉&#xff0c;但是大家的心肯定很热吧&#xff0c;因为发生了两件大事&#xff1a;1、双十一买买买&#xff0c;在这个让…

.NET Core 如何生成真正的ICO图标

点击上方蓝字关注“汪宇杰博客”导语前一阵我终于完成了博客系统动态生成favicon的功能。众所周知&#xff0c;favicon肯定有一个ico格式的图标&#xff0c;其余可以用 png manifest 的方式输出。然而这个ICO格式让我小收福报&#xff0c;今天就给大家分享一下解决办法。.NET自…

Visual Studio Online 东半球首秀,亮相 .NET Conf 2019 中国峰会

佷高兴能参加 .NET Conf 并演讲。看到 NET 社区这么活跃&#xff0c;也是非常开心&#xff01;这次我演讲的主题是《Visual Studio Code —— .NET 开发利器》。找找我在哪&#xff1f;更多关于 Visual Studio Online 的四种开发模式&#xff0c;可以阅读这篇文章&#xff1a;最…

.NET Core 3.0 部署在docker上运行

自从.NET Core3.0发布之后&#xff0c;写了几篇关于.NET Core 3.0的文章&#xff0c;有助于你快速入门.NET Core3.0。本篇文章主要讲解如何一步步创建一个mvc项目&#xff0c;然后发布并部署在Docker上运行。需要你本地有docker环境1.创建一个站点创建一个ASP.NET Core Web应用…

2019 .NET China Conf:路一直都在,社区会更好

这个周末&#xff0c;我从成都飞到了上海参加了首届由社区组织而非官方&#xff08;比如Microsoft&#xff09;组织的.NET开发者峰会&#xff08;.NET Conf&#xff09;。为此&#xff0c;我特意请了两天的假&#xff08;周五周六&#xff0c;对&#xff0c;我们是大小周&#…

“开源、共享、创新”, 中国最具前景开发者峰会落幕魔都

点击蓝字关注我们作者&#xff1a;张善友编辑&#xff1a;吴珊珊校正&#xff1a;潘淳、许豪、刘腾飞、朱兴亮、郑和阳、张潇、韩骏问卷制作&#xff1a;杨乐2019年&#xff0c;注定会是 .NET Core 社区发展的关键一年&#xff0c;诸多重大事件在这一年发生&#xff01;正如大家…

GitHub 2019年度报告,用户超4000万

GitHub 发布了 2019 年年度报告《The State of the Octoverse》&#xff0c;下边来看看一些主要数据。全球用户超过 4 千万 目前 GitHub 上有超过 4000 万开发人员&#xff0c;其中有 80&#xff05; 来自美国以外的地区。去年一年里有 1000 万新加入的开发者&#xff0c;2019 …

github 创建文件夹

https://blog.csdn.net/zhaomengszu/article/details/80354929 在我们不适用本地Git的情况下&#xff0c;我们怎么在网页上创建类似下图一样的文件夹呢 四步方法&#xff1a; 第一步&#xff1a;找到新增按钮 第二步&#xff1a;输入文件夹名,你想要用的文件夹名字。 第三步…

ML.NET 终于在Jupyter NoteBook 上跑起来了

对.NETer来说&#xff0c;刚结束的.NET Conf 2019是非常难忘的&#xff0c;毕竟这个个人觉得比微软在中国办的大会更加清真&#xff0c;当然现阶段.NET 已经不单跑在Windows的一项技术了&#xff0c;它可以跑在Linux/macOS/iOS/Android/IoT等&#xff0c;也可以融合当今最热门的…

2019.NET Conf China,.NET之崛起,已势不可挡

本文来自DotNET技术圈作者&#xff1a;邹溪源一、背景当今时代&#xff0c;气象更新&#xff0c;技术飞速发展。当今时代&#xff0c;开发者大概是最优秀的群体。每一位开发者&#xff0c;无不奋勇向前&#xff0c;努力追寻时代的步伐&#xff0c;以大无畏的精神迎接挑战&#…

PowerBI 11月更新 数据PPT是否会引领新一轮革命

PowerBI Desktop 2019年11月更新新鲜出炉了&#xff1a;罗叔作为全球第 80 人观看更新内容者&#xff0c;快速将本月更新全部奉上&#xff0c;供大家参考。 打开预览特性&#xff0c;如下&#xff1a;本次更新出了三大功能&#xff1a; Power Query 支持 AI新的功能区&#xff…

WeihanLi.Npoi 近期更新

WeihanLi.Npoi 近期更新Intro最近对我的 NPOI 扩展做了一些改变&#xff0c;一方面提高性能&#xff0c;一方面修复bug&#xff0c;增加一些新的功能来让它更加好用&#xff0c;前几天发布了 1.5.0 版本&#xff0c;下面来介绍一下最近的更新默认导入/导出格式变更在 1.5.0 版本…

【.NETCore 3】Ids4 ║ 多项目集成统一认证中心的思考

前言哈喽大家好&#xff0c;好久都没有写文章了&#xff0c;这次又重新开始写技术文章了&#xff0c;半年前我还是一直保持每周都写文章的&#xff0c;后来是为了响应群友的号召&#xff0c;开始踏上了录制视频&#xff08;https://www.bilibili.com/video/av58096866&#xff…