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 something this._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 单元测试