Click here to Skip to main content
13,900,395 members
Click here to Skip to main content
Add your own
alternative version

Stats

12K views
349 downloads
12 bookmarked
Posted 8 Apr 2018
Licenced CPOL

Testing ASP.NET Core MVC Application with NUnit and Moq

, 8 Apr 2018
Rate this:
Please Sign up or sign in to vote.
The how-to article that summarises best practices for unit testing ASP NET Core MVC controller using NUnit and Moq framework

Introduction

Recently, during web development, I had a need to do unit testing of the ASP.NET Core MVC controller. The dotnet core has significant differences compared to the ASP.NET MVC. That's why unit testing is a little bit tricky. After successfully making unit tests, I decided to extract the most important and complex turnovers to share in this article.

Key Points

  • Setup and configuration of the ASP.NET Core (netcoreapp1.1 or netcoreapp2.0) environment for running a Controller
  • Supplying a valid Claims Principal (User Identity)
  • Injecting HttpContext mock into Controller instance

Background

Getting into DotNet Core (netcoreapp1.1 or netcoreapp2.0) unit testing. This article describes how to setup the http context, mock request, response, signIn manager and user manager, loggers and finally instantiate correctly the MVC Controller engaging the IoC (dependency injection). It also includes description of the workaround that I used for validating internal application logic execution using ASP.NET Core MVC logging. For the unit test, the most convenient is NUnit Framework, for mocking purposes, I used Moq framework, since almost all other mocking frameworks are not compatible with DotNet Core ((( .

Using the Code

Supposedly, you have a web application, built with DotNet Core (netcoreapp1.1 or netcoreapp2.0) framework, (using Microsoft.NET.Sdk.Web), now you have to write basic unit tests to make sure that the most important features are functioning correctly after applying any further modifications. In order to make MVC controller function in a proper way (during the unit test), it needs to instantiate and configure the key components (the execution context):

  • IConfigurationRoot (creates from ConfigurationBuilder)
  • IServiceProvider (creates from ServiceCollection)
  • ILoggerFactory
  • UserManager
  • HttpContext (to access the Request, Response, current User and other controller properties)

The most helpful file during the setup step is Startup.cs, which is created from template when new ASP.NET Core MVC application is initialized. This file contains almost all the needed information about the context setup.

Some of the key components can be instantiated and initialized in a natural way, while others (for example, UserManager) should be mocked using the Moq framework.

For executing NUnit tests in the DotNet Core project, package reference to the NUnit3TestAdapter is required!

For the netcoreapp2.0:

<PackageReference Include="NUnit3TestAdapter" Version="3.8.0" />

For the netcoreapp1.1:

<PackageReference Include="NUnit3TestAdapter" Version="3.8.0-alpha1" />

Firstly, the unit tests file has to contain a method, marked with NUnit attribute: [OneTimeSetUp] - this method is called once for all included tests. Inside this method, the context setup is done.

The base component IConfigurationRoot should be instantiated by ConfigurationBuilder:

IConfiguration configuration =  = new ConfigurationBuilder()
      .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
      .AddEnvironmentVariables()
      .Build();

Next step is services setup. Services, such as Logging, Application Settings, EmailSender, etc. When services are configured, IServicesProvider instance should be created.

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

At this point, the IServiceProvider is created, so ILoggerFactory can be resolved and configured with default MVC loggers:

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

As a point of interest, in my unit tests I used a custom logger, to verify that internally application successfully executing some operations, e.g., delivers emails. The custom logger adds messages to a dictionary, defined in the test class, so it's easy to validate programmatically that some internal treatment has been successfully committed, by checking the log dictionary. The step below is not necessary, though it's very helpful in case, when it needs to validate internal logic.

List<TestsLoggerEvent> testLogsStore = new List<TestsLoggerEvent>();
loggerFactory.AddTestsLogger(testLogsStore);

The next component of the context setup is UserManager, optionally it can be mocked as it's shown below:

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);

Any custom implementation of the registered service should be resolved by following the IoC pattern:

var emailSender = serviceProvider.GetService<IEmailSender>();

The IHostingEnvironment can be simply mocked (though it depends on the logic of your controller):

var mockHostingEnvironment = new Mock<IHostingEnvironment>(MockBehavior.Strict);

Setting up the HttpContext is a solid part of this whole treatment, because it defines access to the core controller properties: Request, Response, current User, etc. The valid claims principal is required in order to be able to use User property inside the Controller code. This workaround allows to bypass the login and authentication mechanism and use user principal explicitly, believing that the user has successfully passed the authentication procedure.

// 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);

The EF context can be created as it's shown:

var context = new ApplicationDbContext(
                new DbContextOptionsBuilder<ApplicationDbContext>()
                .UseSqlServer(" ... connection string here ... ").Options);

Once all major components are configured, the controller can be instantiated in the following way:

var controller = new HomeController(
                options,
                context,
                mockUserManager.Object,
                null,
                emailSender,
                loggerFactory, mockHostingEnvironment.Object, configuration);
            controller.ControllerContext = new ControllerContext()
            {
                HttpContext = mockHttpContext.Object
            };

The tricky part of the code above is HttpContext mock injection into the mvc controller. The web context mock is injected through the ControllerContext property.

In the test method, the corresponding controller action can be called with required parameters. The result should be validated using Assert class of the NUint test framework. I won't describe here how to use the Assert class for verifications of the controller action result, because it's a well known, widely used treatment. The downloadable source contains workable project with simple tests of the default HomeController.

Finally, the unit test class should look like this:

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; });
        }
    }
}

The test project in Visual Studio should be similar to what is shown in the screenshot:

Points of Interest

There might be some situations when the result of the executing operation is not accessible explicitly, for example, when sending emails, there is no simple way to make sure inside the unit test that the email message has been successfully delivered to the recipient. The only quick solution is SMTP server response validation. The SMTP server response can be written to the custom logs store, and after that easily verified inside the unit test. The IoC based nature of the .NET Core MVC gives a very convenient way for duing this. It needs to define a custom logger and register it in the ServicesProvider.LoggerFactory. With this, you can get the logger in the application class using LoggerFactory (instantiated and injected automatically) and write messages to the log. Since the custom log is registered only in the tests, it won't affect the application functionality. There is no need to write a custom application login module, or involve third party logging frameworks. The extendable, flexible logging is already a part of the ASP.NET Core MVC.

For extending the ASP.NET MVC logger, it needs to create a class that implements the Microsoft.Extensions.Logging.ILogger interface. And then, register it in the LoggerFactory:

List<TestsLoggerEvent> testLogsStore = new List<TestsLoggerEvent>(); 
loggerFactory.AddTestsLogger(testLogsStore); // using the extension method here, 
                                             // the code is in the attached source file

For more details about custom ASP.NET logger implementation, please refer to the attached sources.

In case there are application configuration files included into the Test project, the Build Action for the config file has to be set to Content.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Sem Shekhovtsov
Software Developer (Senior)
Indonesia Indonesia
I'm professional full stack web developer, nine years of experience. I have good experience with PHP, Java, .Net, NodeJs, Ruby on Rails, JavaScript, LESS, CSS3, HTML5, jQuery, CoffeeScript, ExtJS, Backbone, AngularJS, AJAX, Amazon s3 services, Azure. Programming is not just my work, but more like a hobby. I'm glad to learn new pattern and technologies, making research and study new things. I work remotely for couple of years. More than nine years I'm writing commercial projects.

You may also be interested in...

Pro
Pro

Comments and Discussions

 
PraiseThanks! Pin
Kajetan Kazimierczak15-Apr-18 11:05
memberKajetan Kazimierczak15-Apr-18 11:05 
GeneralMy vote of 5 Pin
Hyland Computer Systems10-Apr-18 4:26
memberHyland Computer Systems10-Apr-18 4:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01 | 2.8.190306.1 | Last Updated 8 Apr 2018
Article Copyright 2018 by Sem Shekhovtsov
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid