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

目录

介绍

背景

使用代码

兴趣点


该文章总结了使用NUnit和Moq框架对ASP NET Core MVC控制器进行单元测试的最佳实践

Download netcoreapp2.0 unit tests sample - 1.4 MB

Download netcoreapp1.1 unit tests sample - 2.1 MB

 

介绍

最近,在Web开发期间,我需要对ASP.NET Core MVC控制器进行单元测试。与ASP.NET MVC相比,.Net Core有着重大的差异。这就是为什么单元测试有点棘手。在成功地进行了单元测试之后,我决定提取出本文中要共享的最重要和最复杂的错误。

关键点

  1. 设置和配置用于运行Controller ASP.NET Corenetcoreapp1.1 netcoreapp2.0)环境
  2. 提供有效的声明的主体(用户身份)
  3. HttpContextmock注入Controller实例中

 

背景

进入DotNet Corenetcoreapp1.1 netcoreapp2.0)单元测试。本文介绍如何设置http上下文,模拟请求,响应,登录管理器和用户管理器,记录器,并最终正确实例化MVC控制器参与IoC(依赖注入)。它还包括我用于使用ASP.NET Core MVC日志记录验证内部应用程序逻辑执行的变通方法的描述。对于单元测试,最方便的是NUnit Framework,出于模拟目的,我使用了Moq框架,因为几乎所有其他模拟框架都与DotNet Core不兼容。

 

使用代码

据说,你有一个使用DotNet Corenetcoreapp1.1 netcoreapp2.0)框架(使用Microsoft.NET.Sdk.Web)构建的Web应用程序,现在你必须编写基本的单元测试,以确保在应用任何进一步修改后,其最重要的功能正常运行。为了使MVC控制器以适当的方式运行(在单元测试期间),它需要实例化和配置关键组件(执行上下文):

  1. IConfigurationRoot (从ConfigurationBuilder中创建)
  2. IServiceProvider (从ServiceCollection中创建)
  3. ILoggerFactory
  4. UserManager
  5. HttpContext (访问请求、响应、当前用户和其他控制器属性)

设置步骤中最有用的文件是Startup.cs,它是在初始化新的ASP.NET Core MVC应用程序时从模板创建的。此文件包含有关上下文设置的几乎所有必需信息。

一些关键组件可以以自然方式实例化和初始化,而其他组件(例如UserManager)应该使用Moq框架进行模拟。

要在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();

下一步是服务设置。服务,例如日志记录,应用程序设置,邮件发送者等。配置服务时,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记录器进行解析和配置:

var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
loggerFactory.AddConsole(configuration.GetSection("Logging"));
loggerFactory.AddDebug();

作为一个兴趣点,在我的单元测试中,我使用了自定义记录器,以验证内部应用程序是否成功执行了某些操作,例如,发送电子邮件。自定义记录器将消息添加到测试类中定义的字典中,因此通过检查日志字典,可以通过编程方式验证已成功提交某些内部处理。下面的步骤不是必需的,尽管它需要验证内部逻辑时非常有用。

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 的控制器编码的内部属性。此解决方法允许绕过登录和身份验证机制并明确使用用户主体,相信用户已成功通过身份验证过程。

// 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上下文:

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上下文模拟通过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中的测试项目应类似于屏幕截图中显示的内容:

https://www.codeproject.com/KB/aspnet/1238474/sample_dot_net_core_unit_tests.jpg

兴趣点

在某些情况下,执行操作的结果无法明确访问,例如,在发送电子邮件时,没有简单的方法可以确保在单元测试中电子邮件已成功传递给收件人。唯一的快速解决方案是SMTP服务器响应验证。SMTP服务器响应可以写入自定义日志存储,然后在单元测试中轻松验证。基于IoC.NET Core MVC特性为此提供了一种非常方便的方法。它需要定义一个自定义记录器并在其中注册ServicesProvider.LoggerFactory。有了这个,您可以使用应用程序类中的记录器LoggerFactory(实例化并自动注入)并将消息写入日志。由于自定义日志仅在测试中注册,因此不会影响应用程序功能。无需编写自定义应用程序登录模块,也不需要涉及第三方日志记录框架。可扩展,灵活的日志记录已经是ASP.NET Core MVC的一部分。

为了扩展ASP.NET MVC记录器,它需要创建一个实现它的类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记录器实现的更多详细信息,请参阅附带的源代码。

如果测试项目中包含应用程序配置文件,则必须将配置文件的构建操作设置为Content

https://www.codeproject.com/KB/aspnet/1238474/ConfigFileBuildAction.jpg

 

原文链接:https://www.codeproject.com/Articles/1238474/Testing-ASP-NET-Core-MVC-Application-with-NUnit-an

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值