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

点点圈 提交于 2019-12-03 05:37:46

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<IAuthenticationService>().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<IAuthenticationService>();
authenticationServiceMock
    .Setup(a => a.SignInAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<AuthenticationProperties>()))
    .Returns(Task.CompletedTask);

var serviceProviderMock = new Mock<IServiceProvider>();
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<IUrlHelperFactory>();
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.

Is this a DI related issue (something needed that isn't being provided in the test but is in normal execution)?

YES

You are calling features that the framework would setup for you at run time. During isolated unit tests you will need to set these up yourself.

The Controller's HttpContext is missing an IServiceProvider which it uses to resolve IAuthenticationService. That service is what actually calls SignInAsync

In order to let....

await HttpContext.SignInAsync(principal);  // FAILS HERE

...in the Registration action to execute to completion during the unit test you will need to mock a service provider so that the SignInAsync extension method does not fail.

Update the unit test arrangement

//...code removed for brevity

auth.ControllerContext.HttpContext = new DefaultHttpContext() {
    RequestServices = createServiceProviderMock()
};

//...code removed for brevity

Where createServiceProviderMock() is a small method used to mock a service provider that will be used to populate the HttpContext.RequestServices

public IServiceProvider createServiceProviderMock() {
    var authServiceMock = new Mock<IAuthenticationService>();
    authServiceMock
        .Setup(_ => _.SignInAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<AuthenticationProperties>()))
        .Returns(Task.FromResult((object)null)); //<-- to allow async call to continue

    var serviceProviderMock = new Mock<IServiceProvider>();
    serviceProviderMock
        .Setup(_ => _.GetService(typeof(IAuthenticationService)))
        .Returns(authServiceMock.Object);

    return serviceProviderMock.Object;
}

I would also suggest mocking the Repository for the purposes of an isolated unit test of that controller action to make sure it flows to completion without any negative effects

as @poke mentioned you better not use Dependency Injection in unit tests and provide dependencies explicitly (using mocking) but however, I had this issue in my integration tests and I figured that the problem arises from RequestServices property of HttpContext which is not properly initialized in tests (since we don't use actual HttpContext in tests) so I registered my HttpContextAccessor like below and passed all of it's required service myself (manually) and problem solved. see code below

Services.AddSingleton<IHttpContextAccessor>(new HttpContextAccessor() { HttpContext = new DefaultHttpContext() { RequestServices = Services.BuildServiceProvider() } });

I agree it's not a very clean solution but note that I wrote and used this code only in my tests in order to provide required HttContext dependencies (which were not supplied automatically in test method), in your application IHttpContextAccessor, HttpContext and their required services are automatically provided by framework.

here is all of my dependency registration method in my tests base class constructor

 public class MyTestBaseClass
 {
  protected ServiceCollection Services { get; set; } = new ServiceCollection();
  MyTestBaseClass
 {

   Services.AddDigiTebFrameworkServices();
        Services.AddDigiTebDBContextService<DigiTebDBContext> 
        (Consts.MainDBConnectionName);
        Services.AddDigiTebIdentityService<User, Role, DigiTebDBContext>();
        Services.AddDigiTebAuthServices();
        Services.AddDigiTebCoreServices();
        Services.AddSingleton<IHttpContextAccessor>(new HttpContextAccessor() { HttpContext = new DefaultHttpContext() { RequestServices = Services.BuildServiceProvider() } });
}
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!