背景

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. 新人问题:为何要写单元测试?

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

Asp.Net Core 单元测试正确姿势相关推荐

  1. ASP.NET Core 单元测试:如何 Mock HttpContext.Features.Get()

    点击上方蓝字关注"汪宇杰博客" 导语 在 ASP.NET Core 里,如果你想单元测试 HttpContext.Features.Get<SomeType>(),这个 ...

  2. 关于单元测试的思考--Asp.Net Core单元测试最佳实践

    https://www.cnblogs.com/yubaolee/p/DotNetCoreUnitTest.html 在我们码字过程中,单元测试是必不可少的.但在从业过程中,很多开发者却对单元测试望而 ...

  3. ASP.NET Core 单元测试:如何Mock Url.Page()

    点击上方蓝字关注"汪宇杰博客" 导语 在 ASP.NET Core 中,当你在 UrlHelperExtensions 类上使用扩展方法时,很难在单元测试中编写Mock.因为Moq ...

  4. 撸.NET Core的正确姿势

    特点 案例基于刚发布的.NET Core 2.1 只需一台Linux服务器搞定一切, 全程无需自己配置dotnet环境, 需要熟悉git docker基础知识可有可无, 过了下面几个步骤,你就已经入门 ...

  5. 深入探究ASP.NET Core读取Request.Body的正确方式

    前言 相信大家在使用ASP.NET Core进行开发的时候,肯定会涉及到读取Request.Body的场景,毕竟我们大部分的POST请求都是将数据存放到Http的Body当中.因为笔者日常开发所使用的 ...

  6. ASP.NET Core 对Controller进行单元测试

    单元测试对我们的代码质量非常重要.很多同学都会对业务逻辑或者工具方法写测试用例,但是往往忽略了对Controller层写单元测试.我所在的公司没见过一个对Controller写过测试的.今天来演示下如 ...

  7. 以正确的方式下载和配置 ASP.NET Core 官方源码

    我们可以在Github上面直接查看ASP.NETCore 3.x的源代码,但是我们也可以把源代码下载下来进行查看. 而下载源代码进行查看有很多好处: 任意的导航源代码 内置了一个示例项目 直接调试源代 ...

  8. .NET Core 使用 K8S ConfigMap的正确姿势

    背景 ASP.NET Core默认的配置文件定义在 appsetings.json和 appsettings.{Environment}.json文件中.这里面有一个问题就是,在使用容器部署时,每次修 ...

  9. Asp.Net Core 轻松学-正确使用分布式缓存

    前言     本来昨天应该更新的,但是由于各种原因,抱歉,让追这个系列的朋友久等了.上一篇文章 在.Net Core 使用缓存和配置依赖策略 讲的是如何使用本地缓存,那么本篇文章就来了解一下如何使用分 ...

最新文章

  1. mysql 释放错误连接_JSP连接MySQL后数据库链接释放的错误
  2. 两位智源青年科学家榜上有名!2020青橙奖公布
  3. 深度学习模型的中毒攻击与防御综述
  4. python练习程序(批量重命名)
  5. rust(20)-字符
  6. 如何在 CSS 中设置组件在浏览器屏幕水平垂直居中
  7. 如何实现从wgs-84到beijing54的坐标转换
  8. Python——列表中存放字典遇到的问题
  9. 大数据时代,怎么做全渠道的营销
  10. Python文件拷贝函数
  11. Google AdSense广告被屏蔽
  12. [LeetCode]235.Lowest Common Ancestor of a Binary Search Tree
  13. L1-026 I Love GPLT
  14. 使用UE4基于Hololens开发MR应用
  15. gii无法访问 yii2_Gii的CURD生成无法访问?
  16. 从来不敷面膜的人_女人一旦过了40岁,敷面膜要记住“3不要”,否则还不如不敷!...
  17. 计算机安装操作步骤,重新安装计算机系统的步骤,最简单,最安全的操作!
  18. 基础、开发者、智能合约……统统都是矩阵元未来的关键词
  19. 搜狗拼音皮肤 php文件,搜狗拼音输入法皮肤制作
  20. 还原一个真实的银行待遇

热门文章

  1. IIS相关问题及解决方案
  2. 如何使计算机为您读取文档
  3. Cocos Creator Ui系统
  4. List 分页加载数据控制机制
  5. mybatis由浅入深day01_5mybatis开发dao的方法(5.1SqlSession使用范围_5.2原始dao开发方法)...
  6. PHP自动查找指定文件夹下所有文件BOM和删除所有文件
  7. [单刷 APUE 系列] 第十四章——高级 I/O
  8. Python学习笔记整理(三)Python中的动态类型简介
  9. 在C#中使用SQLite
  10. 如何理解 ListT和 DictionaryK,V 的扩容机制 ?