65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2018年4月8日

CPOL

5分钟阅读

viewsIcon

35110

downloadIcon

800

这篇操作指南总结了使用 NUnit 和 Moq 框架对 ASP NET Core MVC 控制器进行单元测试的最佳实践。

引言

最近,在 Web 开发过程中,我需要对 ASP.NET Core MVC 控制器进行单元测试。 dotnet core 与 ASP.NET MVC 相比有显著差异。因此,单元测试有点棘手。在成功完成单元测试后,我决定提取最重要和最复杂的转折点,并在本文中分享。

要点

  • 设置和配置 ASP.NET Core (netcoreapp1.1netcoreapp2.0) 环境以运行控制器
  • 提供有效的 Claims Principal (用户身份)
  • HttpContext 模拟注入控制器实例

背景

深入 DotNet Core (netcoreapp1.1netcoreapp2.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.1netcoreapp2.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 是整个处理过程的重要组成部分,因为它定义了对核心控制器属性的访问:RequestResponse、当前 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

© . All rights reserved.