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.
However, testing the authentication/authorization (does a user get logged in and can access [Authorize]
filtered actions) is failing with...
System.ArgumentNullException: Value cannot be null.
Parameter name: provider
...after...
await HttpContext.SignInAsync(principal);
...but it is not clear what in fact is the underlying cause. Code execution stops in the called method here and no exception is shown in the IDE but code execution returns to the caller, then terminates (yet I still see The program '[13704] dotnet.exe' has exited with code 0 (0x0).
in the output window of VS.)
The Test Explorer shows red and gives the exception referenced (otherwise I would have no idea as to the problem.)
I am working on creating a repro to point folks to (turning out to bit a bit involved thus far.)
Does anyone know how to pinpoint the underlying cause? Is this a DI related issue (something needed that isn't being provided in the test but is in normal execution)?
UPDATE1: Providing requested authentication code...
public async Task<IActionResult> Registration(RegistrationViewModel vm) {
if (ModelState.IsValid) {
// Create registration for user
var regData = createRegistrationData(vm);
_repository.AddUserRegistrationWithGroup(regData);
var claims = new List<Claim> {
new Claim(ClaimTypes.NameIdentifier, regData.UserId.ToString())
};
var ident = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(ident);
await HttpContext.SignInAsync(principal); // FAILS HERE
return RedirectToAction("Welcome", "App");
} else {
ModelState.AddModelError("", "Invalid registration information.");
}
return View();
}
The test code that fails...
public async Task TestRegistration()
{
var ctx = Utils.GetInMemContext();
Utils.LoadJsonData(ctx);
var repo = new Repository(ctx);
var auth = new AuthController(repo);
auth.ControllerContext = new ControllerContext();
auth.ControllerContext.HttpContext = new DefaultHttpContext();
var vm = new RegistrationViewModel()
{
OrgName = "Dev Org",
BirthdayDay = 1,
BirthdayMonth = "January",
BirthdayYear = 1979
};
var orig = ctx.Registrations.Count();
var result = await auth.Registration(vm); // STEPS IN, THEN FAILS
var cnt = ctx.Registrations.Count();
var view = result as ViewResult;
Assert.AreEqual(0, orig);
Assert.AreEqual(1, cnt);
Assert.IsNotNull(result);
Assert.IsNotNull(view);
Assert.IsNotNull(view.Model);
Assert.IsTrue(string.IsNullOrEmpty(view.ViewName) || view.ViewName == "Welcome");
}
UPDATE3: Based on chat @nkosi suggested that this is a problem stemming from my not fulfilling the needs of the dependency injection requirements for HttpContext
.
However, 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?
UPDATE4: While @Nkosi answered the bug/problem with a solution, 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? Why aren't exceptions "popping" like I would expect while debugging the test, and the exit code is still zero (typically indicating a successful return state)?
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() } });
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With