使用 NUnit 和 Moq 测试 ASP.NET Core MVC 应用程序





5.00/5 (4投票s)
这篇操作指南总结了使用 NUnit 和 Moq 框架对 ASP NET Core MVC 控制器进行单元测试的最佳实践。
引言
最近,在 Web 开发过程中,我需要对 ASP.NET Core MVC 控制器进行单元测试。 dotnet core 与 ASP.NET MVC 相比有显著差异。因此,单元测试有点棘手。在成功完成单元测试后,我决定提取最重要和最复杂的转折点,并在本文中分享。
要点
- 设置和配置 ASP.NET Core (
netcoreapp1.1
或netcoreapp2.0
) 环境以运行控制器 - 提供有效的 Claims Principal (用户身份)
- 将
HttpContext
模拟注入控制器实例
背景
深入 DotNet Core (netcoreapp1.1
或 netcoreapp2.0
) 单元测试。本文介绍了如何设置 http 上下文、模拟请求、响应、signIn manager 和 user manager、loggers,并最终正确实例化 MVC 控制器,启用 IoC (依赖注入)。它还包括了我用于验证内部应用程序逻辑执行的 ASP.NET Core MVC 日志记录的解决方法。对于单元测试,最方便的是 NUnit Framework,对于模拟,我使用了 Moq framework,因为几乎所有其他模拟框架都与 DotNet Core 不兼容 (((。
Using the Code
假设您有一个使用 DotNet Core (netcoreapp1.1
或 netcoreapp2.0
) 框架构建的 Web 应用程序 (使用 Microsoft.NET.Sdk.Web
),现在您需要编写基本的单元测试,以确保在进行任何进一步修改后,最重要的功能都能正常运行。为了使 MVC 控制器在单元测试期间正常工作,它需要实例化和配置关键组件 (执行上下文)。
IConfigurationRoot
(从ConfigurationBuilder
创建)IServiceProvider
(从ServiceCollection
创建)ILoggerFactory
UserManager
HttpContext
(用于访问 Request, Response, 当前 User 等控制器属性)
在设置步骤中最有用的文件是 Startup.cs,当新的 ASP.NET Core MVC 应用程序初始化时,它会从模板创建。此文件包含几乎所有所需的上下文设置信息。
一些关键组件可以自然实例化和初始化,而另一些组件 (例如 UserManager
) 则应使用 Moq framework 进行模拟。
要在 DotNet Core 项目中执行 NUnit 测试,需要包引用 NUnit3TestAdapter!
对于 netcoreapp2.0
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />
对于 netcoreapp1.1
<PackageReference Include="NUnit3TestAdapter" Version="3.8.0-alpha1" />
首先,单元测试文件必须包含一个标记为 NUnit 属性的方法:[OneTimeSetUp]
- 此方法对所有包含的测试只调用一次。在此方法内部完成上下文设置。
基本组件 IConfigurationRoot
应由 ConfigurationBuilder
实例化。
IConfiguration configuration = = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
下一步是服务设置。服务,例如 Logging、Application Settings、EmailSender 等。配置服务后,应创建 IServicesProvider
实例。
var services = new ServiceCollection();
services.AddLogging();
services.AddMvc().AddJsonOptions(jsonOptions =>
{
jsonOptions.SerializerSettings.ContractResolver =
new Newtonsoft.Json.Serialization.DefaultContractResolver();
});
services.AddOptions();
services.Configure<AppSettings>(configuration.GetSection("AppSettings"));
services.AddSingleton<IConfiguration>(configuration);
services.AddTransient<IEmailSender, MessageServices>();
var serviceProvider = services.BuildServiceProvider();
此时,已创建 IServiceProvider
,因此可以解析 ILoggerFactory
并使用默认的 MVC loggers 进行配置。
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
loggerFactory.AddConsole(configuration.GetSection("Logging"));
loggerFactory.AddDebug();
有趣的是,我在单元测试中使用了自定义 logger,以验证应用程序内部是否成功执行了某些操作,例如发送电子邮件。自定义 logger 将消息添加到测试类中定义的字典中,因此可以通过检查 log 字典轻松地以编程方式验证某些内部处理是否已成功提交。虽然此步骤不是必需的,但如果需要验证内部逻辑,它非常有帮助。
List<TestsLoggerEvent> testLogsStore = new List<TestsLoggerEvent>();
loggerFactory.AddTestsLogger(testLogsStore);
上下文设置的下一个组件是 UserManager
,可以根据需要进行模拟,如下所示。
var userManagerLogger = loggerFactory.CreateLogger<UserManager<ApplicationUser>>();
var mockUserManager = new Mock<UserManager<ApplicationUser>>(MockBehavior.Default,
new Mock<IUserStore<ApplicationUser>>().Object,
new Mock<IOptions<IdentityOptions>>().Object,
new Mock<IPasswordHasher<ApplicationUser>>().Object,
new IUserValidator<ApplicationUser>[0],
new IPasswordValidator<ApplicationUser>[0],
new Mock<ILookupNormalizer>().Object,
new Mock<IdentityErrorDescriber>().Object,
new Mock<IServiceProvider>().Object,
userManagerLogger);
已注册服务的任何自定义实现都应遵循 IoC 模式进行解析。
var emailSender = serviceProvider.GetService<IEmailSender>();
IHostingEnvironment
可以简单地进行模拟 (尽管这取决于控制器的逻辑)。
var mockHostingEnvironment = new Mock<IHostingEnvironment>(MockBehavior.Strict);
设置 HttpContext
是整个处理过程的重要组成部分,因为它定义了对核心控制器属性的访问:Request
、Response
、当前 User
等。为了能够在控制器代码中使用 User
属性,需要有效的 claims principal。此解决方法允许绕过登录和身份验证机制,并显式使用用户 principal,假设用户已成功通过身份验证过程。
// the user principal is needed to be able to use
// the User.Identity.Name property inside the controller
var validPrincipal = new ClaimsPrincipal(
new[]
{
new ClaimsIdentity(
new[] {new Claim(ClaimTypes.Name, "... auth login name or email ...") })
});
var mockHttpContext = new Mock<HttpContext>(MockBehavior.Strict);
mockHttpContext.SetupGet(hc => hc.User).Returns(validPrincipal);
mockHttpContext.SetupGet(c => c.Items).Returns(httpContextItems);
mockHttpContext.SetupGet(ctx => ctx.RequestServices).Returns(serviceProvider);
var collection = Mock.Of<IFormCollection>();
var request = new Mock<HttpRequest>();
request.Setup(f => f.ReadFormAsync(CancellationToken.None)).Returns
(Task.FromResult(collection));
// setting up any other used property or function of the HttpRequest
mockHttpContext.SetupGet(c => c.Request).Returns(request.Object);
var response = new Mock<HttpResponse>();
response.SetupProperty(it => it.StatusCode);
// setting up any other used property or function of the HttpResponse
mockHttpContext.Setup(c => c.Response).Returns(response.Object);
EF context 可以按如下方式创建。
var context = new ApplicationDbContext(
new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlServer(" ... connection string here ... ").Options);
一旦所有主要组件都配置完成,就可以按照以下方式实例化控制器。
var controller = new HomeController(
options,
context,
mockUserManager.Object,
null,
emailSender,
loggerFactory, mockHostingEnvironment.Object, configuration);
controller.ControllerContext = new ControllerContext()
{
HttpContext = mockHttpContext.Object
};
上面的代码中棘手的部分是将 HttpContext
模拟注入 mvc 控制器。Web context 模拟通过 ControllerContext
属性注入。
在测试方法中,可以使用所需的参数调用相应的控制器操作。结果应使用 NUint 测试框架的 Assert
类进行验证。我不会在这里描述如何使用 Assert
类来验证控制器操作结果,因为它是一个众所周知且广泛使用的处理方法。可下载的源包含一个可运行的项目,其中包含对默认 HomeController
的简单测试。
最后,单元测试类应该如下所示。
using ...;
namespace UnitTestsSample
{
[TestFixture]
public class UnitTest1
{
private HomeController _controller;
private Dictionary<object, object> _httpContextItems = new Dictionary<object, object>();
private List<TestsLoggerEvent> _testLogsStore = new List<TestsLoggerEvent>();
[OneTimeSetUp]
public void TestSetup()
{
var configuration = ...;
var services = ...;
var serviceProvider = services.BuildServiceProvider();
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
...
var userManagerLogger = loggerFactory.CreateLogger<UserManager<ApplicationUser>>();
var mockUserManager = ....;
...
var emailSender = serviceProvider.GetService<IEmailSender>();
var mockHostingEnvironment = new Mock<IHostingEnvironment>(MockBehavior.Strict);
var validPrincipal = new ClaimsPrincipal(
new[]
{
new ClaimsIdentity(
new[] {new Claim(ClaimTypes.Name, "testsuser@testinbox.com") })
});
var mockHttpContext = ...;
_controller = new HomeController(
options,
context,
mockUserManager.Object,
null,
emailSender,
loggerFactory,
mockHostingEnvironment.Object,
configuration);
_controller.ControllerContext = new ControllerContext()
{
HttpContext = mockHttpContext.Object
};
}
[Test]
public void TestAboutAction()
{
var controllerActionResult = _controller.About();
Assert.IsNotNull(controllerActionResult);
Assert.IsInstanceOf<ViewResult>(controllerActionResult);
var viewResult = controllerActionResult as ViewResult;
Assert.AreSame(viewResult.ViewData["Message"], "Your application description page.");
Assert.AreSame(_controller.User.Identity.Name, "testsuser@testinbox.com");
Assert.AreSame
(_controller.Request.Headers["X-Requested-With"].ToString(), "XMLHttpRequest");
Assert.DoesNotThrow(() => { _controller.Response.StatusCode = 500; });
}
}
}
Visual Studio 中的测试项目应该与屏幕截图所示类似。
关注点
在某些情况下,执行操作的结果可能无法显式访问,例如,在发送电子邮件时,在单元测试中无法轻松确保电子邮件消息已成功发送到收件人。唯一的快速解决方案是 SMTP 服务器响应验证。SMTP 服务器响应可以写入自定义日志存储,然后轻松地在单元测试中进行验证。DotNET Core MVC 的 IoC 基于的性质为实现此目的提供了非常方便的方式。需要定义一个自定义 logger 并将其注册到 ServicesProvider.LoggerFactory
。通过这种方式,您可以使用 LoggerFactory
(自动实例化和注入) 在应用程序类中获取 logger,并将消息写入日志。由于自定义日志仅在测试中注册,因此它不会影响应用程序的功能。无需编写自定义应用程序登录模块,或涉及第三方日志框架。可扩展、灵活的日志记录已经是 ASP.NET Core MVC 的一部分。
要扩展 ASP.NET MVC logger,需要创建一个实现 Microsoft.Extensions.Logging.ILogger
接口的类。然后,将其注册到 LoggerFactory
。
List<TestsLoggerEvent> testLogsStore = new List<TestsLoggerEvent>();
loggerFactory.AddTestsLogger(testLogsStore); // using the extension method here,
// the code is in the attached source file
有关自定义 ASP.NET logger 实现的更多详细信息,请参阅附加的源文件。
如果测试项目包含应用程序配置文件,则配置文件的 Build Action 必须设置为 Content
。