ASP.Net Core 2.0 SignInAsync returns exception Value cannot be null, provider

后端 未结 3 698
感情败类
感情败类 2021-02-07 07:53

I have an ASP.Net Core 2.0 web application I am retrofitting with unit tests (using NUnit). The application works fine, and most of the tests thus far work fine.

Howeve

3条回答
  •  旧时难觅i
    2021-02-07 08:11

    What isn't yet clear is: if it is, in fact, an issue of not providing the proper service dependency, why does the code work normally (when not being tested). The SUT (controller) only accepts an IRepository parameter (so that is all that is provided in any case.) Why create an overloaded ctor (or mock) just for test, when the existing ctor is all that is called when running the program and it runs without issue?

    You are mixing up a few things here: First of all, you don’t need to create separate constructors. Not for testing, and not for actually running this as part of your application.

    You should define all the direct dependencies your controller has as parameters to the constructor, so that when this runs as part of the application, the dependency injection container will provide those dependencies to the controller.

    But that’s also the important bit here: When running your application, there is a dependency injection container that is responsible of creating objects and providing the required dependencies. So you actually don’t need to worry too much about where they come from. This is different when unit testing though. In unit tests, we don’t want to use dependency injection since that will just hide dependencies, and as such possible side effects that may conflict with our test. Relying on dependency injection within a unit test is a very good sign that you are not unit testing but doing an integration test instead (at least unless you are actually testing a DI container).

    Instead, in unit tests, we want to create all objects explicitly providing all dependencies explicitly. This means that we new up the controller and pass all dependencies the controller has. Ideally, we use mocks so we don’t depend on external behavior in our unit test.

    This is all pretty straight forward most of the time. Unfortunately, there is something special about controllers: Controllers have a ControllerContext property that is automatically provided during the MVC lifecycle. Some other components within MVC have similar things (e.g. the ViewContext is also automatically provided). These properties are not constructor injected, so the dependency is not explicitly visible. Depending on what the controller does, you might need to set these properties too when unit testing the controller.


    Coming to your unit test, you are using HttpContext.SignInAsync(principal) inside your controller action, so unfortunately, you are operating with the HttpContext directly.

    SignInAsync is an extension method which will basically do the following:

    context.RequestServices.GetRequiredService().SignInAsync(context, scheme, principal, properties);
    

    So this method, for pure convenience, will use the service locator pattern to retrieve a service from the dependency injection container to perform the sign-in. So just this one method call on the HttpContext will pull in further implicit dependencies that you only discover about when your test fails. That should serve as a good example on why you should avoid the service locator pattern: Explicit dependencies in the constructor are much more manageable. – But here, this is a convenience method, so we will have to live with that and just adjust the test to work with this.

    Actually, before moving on, I want to mention a good alternative solution here: Since the controller is a AuthController I can only imagine that one of its core purposes is to do authentication stuff, signing users in and out and things. So it might actually be a good idea not to use HttpContext.SignInAsync but instead have the IAuthenticationService as an explicit dependency on the controller, and calling the methods on it directly. That way, you have a clear dependency that you can fulfill in your tests and you don’t need to get involved with the service locator.

    Of course, this would be a special case for this controller and won’t work for every possible call of the extension methods on the HttpContext. So let’s tackle how we can test this properly:

    As we can see from the code what SignInAsync actually does, we need to provide a IServiceProvider for HttpContext.RequestServices and make that be able to return an IAuthenticationService. So we’ll mock these:

    var authenticationServiceMock = new Mock();
    authenticationServiceMock
        .Setup(a => a.SignInAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
        .Returns(Task.CompletedTask);
    
    var serviceProviderMock = new Mock();
    serviceProviderMock
        .Setup(s => s.GetService(typeof(IAuthenticationService)))
        .Returns(authenticationServiceMock.Object);
    

    Then, we can pass that service provider in the ControllerContext after creating the controller:

    var controller = new AuthController();
    controller.ControllerContext = new ControllerContext
    {
        HttpContext = new DefaultHttpContext()
        {
            RequestServices = serviceProviderMock.Object
        }
    };
    

    That’s all we need to do to make HttpContext.SignInAsync work.

    Unfortunately, there is a bit more to it. As I’ve explained in this other answer (which you already found), returning a RedirectToActionResult from a controller will cause problems when you have the RequestServices set up in a unit test. Since RequestServices are not null, the implementation of RedirectToAction will attempt to resolve an IUrlHelperFactory, and that result has to be non-null. As such, we need to expand our mocks a bit to also provide that one:

    var urlHelperFactory = new Mock();
    serviceProviderMock
        .Setup(s => s.GetService(typeof(IUrlHelperFactory)))
        .Returns(urlHelperFactory.Object);
    

    Luckily, we don’t need to do anything else, and we also don’t need to add any logic to the factory mock. It’s enough if it’s just there.

    So with this, we can test the controller action properly:

    // mock setup, as above
    // …
    
    // arrange
    var controller = new AuthController(repositoryMock.Object);
    controller.ControllerContext = new ControllerContext
    {
        HttpContext = new DefaultHttpContext()
        {
            RequestServices = serviceProviderMock.Object
        }
    };
    
    var registrationVm = new RegistrationViewModel();
    
    // act
    var result = await controller.Registration(registrationVm);
    
    // assert
    var redirectResult = result as RedirectToActionResult;
    Assert.NotNull(redirectResult);
    Assert.Equal("Welcome", redirectResult.ActionName);
    

    I am still wondering why the IDE isn't accurately/consistently presenting the underlying exception. Is this a bug, or due to the async/await operators and the NUnit Test Adapter/runner?

    I have seen something similar in the past too with my asynchronous tests, that I could not debug them properly or that exceptions wouldn’t be displayed correctly. I don’t remember seeing this in recent versions of Visual Studio and xUnit (I’m personally using xUnit, not NUnit). If it helps, running the tests from the command line with dotnet test will usually work properly and you will get proper (async) stack traces for failures.

提交回复
热议问题