ASP.Net Core 2.0 SignInAsync返回异常值不能为null,提供者

时间:2018-02-23 01:06:27

标签: c# asp.net-core-2.0 nunit-3.0

我有一个ASP.Net Core 2.0 Web应用程序我正在使用单元测试进行改造(使用NUnit)。该应用程序运行正常,迄今为止大多数测试工作正常。

但是,测试身份验证/授权(用户是否已登录并可以访问[Authorize]已过滤的操作)是否失败...

System.ArgumentNullException: Value cannot be null.
Parameter name: provider

... ...后

await HttpContext.SignInAsync(principal);

......但目前尚不清楚其根本原因是什么。代码执行在此处停止,并且IDE中没有显示异常,但代码执行返回给调用者,然后终止(但我仍然在VS的输出窗口中看到The program '[13704] dotnet.exe' has exited with code 0 (0x0).。)

测试资源管理器显示红色并显示引用的异常(否则我不知道问题。)

我正在努力创建一个指向人们的复制品(到目前为止已经涉及到一点点。)

有谁知道如何查明根本原因?这是与DI相关的问题(测试中是否需要提供但是正常执行)?

UPDATE1:提供请求的身份验证码...

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

失败的测试代码......

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 :基于chat @nkosi suggested,这是由于我无法满足HttpContext的依赖注入要求所带来的问题。

However,尚不清楚的是:如果它实际上是一个不提供正确服务依赖性的问题,为什么代码正常工作(未经测试时)。 SUT(控制器)只接受一个I​​Repository参数(这就是在任何情况下提供的所有参数。)为什么创建一个重载的ctor(或mock)只是为了测试,当现有的ctor是在运行程序时调用的所有ctor和它运行没有问题?

UPDATE4 :虽然@Nkosi用解决方案回答了错误/问题,但我仍然想知道为什么IDE没有准确/一致地呈现基础异常。这是一个错误,还是由于async / await运算符和NUnit Test Adapter / runner?为什么没有例外&#34;弹出&#34;就像我在调试测试时所期望的那样,退出代码仍为零(通常表示成功返回状态)?

3 个答案:

答案 0 :(得分:12)

  

尚不清楚的是:如果它实际上是一个不提供正确服务依赖性的问题,为什么代码正常工作(未经测试时)。 SUT(控制器)只接受一个IRepository参数(这就是在任何情况下提供的所有参数。)为什么创建一个重载的ctor(或mock)只是为了测试,当现有的ctor是所有被调用的时候运行该程序,它运行没有问题?

您在这里混合了一些东西:首先,您不需要创建单独的构造函数。不用于测试,也不用于在应用程序中实际运行它。

您应该将控制器具有的所有直接依赖项定义为构造函数的参数,以便当它作为应用程序的一部分运行时,依赖项注入容器将向控制器提供这些依赖项。

但这也是重要的一点:在运行应用程序时,有一个依赖注入容器,负责创建对象并提供所需的依赖项。所以你实际上并不需要过多担心它们来自哪里。但是在单元测试时这是不同的。在单元测试中,我们不想使用依赖注入,因为这只会隐藏依赖关系,因此可能会产生可能与我们的测试冲突的副作用。在单元测试中依赖依赖注入是一个非常好的迹象,表明你不是单元测试,而是进行集成测试(至少除非你实际测试的是DI容器) )。

相反,在单元测试中,我们希望显式创建所有 对象,明确提供所有依赖项。这意味着我们新建控制器并传递控制器具有的所有依赖项。理想情况下,我们使用模拟,因此我们不依赖于单元测试中的外部行为。

这大部分时间都很直接。不幸的是,控制器有一些特殊之处:控制器具有在MVC生命周期中自动提供的ControllerContext属性。 MVC中的一些其他组件具有类似的功能(例如,ViewContext也会自动提供)。这些属性不是构造函数注入的,因此依赖项不是显式可见的。根据控制器的作用,在单元测试控制器时,您可能还需要设置这些属性。

进行单元测试后,您在控制器操作中使用HttpContext.SignInAsync(principal),不幸的是,您正在使用HttpContext直接操作。

SignInAsyncwill basically do the following

的扩展方法
context.RequestServices.GetRequiredService<IAuthenticationService>().SignInAsync(context, scheme, principal, properties);

因此,为方便起见,此方法将使用service locator pattern从依赖项注入容器中检索服务以执行登录。因此,HttpContext上的这一个方法调用将引入更多隐式依赖关系,您只能在测试失败时发现这些依赖关系。这应该是why you should avoid the service locator pattern的一个很好的例子:构造函数中的显式依赖关系更易于管理。 - 但是在这里,这是一种方便的方法,因此我们将不得不忍受这一点,只需调整测试即可使用。

实际上,在继续之前,我想在这里提一个好的替代解决方案:由于控制器是AuthController我只能想象其核心目的之一是进行身份验证,将用户签入和签出和事情。因此,最好不要使用HttpContext.SignInAsync,而是在控制器上将IAuthenticationService作为显式依赖,并直接调用其上的方法。这样,您就可以在测试中实现明确的依赖关系,而无需参与服务定位器。

当然,这将是此控制器的特殊情况,并且不适用于 HttpContext上每个可能的扩展方法调用。那么让我们来解决我们如何正确测试这个问题:

正如我们从代码中可以看到SignInAsync实际上做了什么,我们需要为IServiceProvider提供HttpContext.RequestServices并使其能够返回IAuthenticationService。所以我们会嘲笑这些:

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

然后,我们可以在创建控制器之后在ControllerContext中传递该服务提供者:

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

我们需要做的就是让HttpContext.SignInAsync工作。

不幸的是,还有更多内容。正如我在this other answer(您已经找到)中所解释的那样,当您在单元测试中设置RedirectToActionResult时,从控制器返回RequestServices会导致问题。由于RequestServices不为null,RedirectToAction的实现将尝试解析IUrlHelperFactory,并且该结果必须为非null。因此,我们需要稍微扩展我们的模拟以提供一个:

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

幸运的是,我们不需要做任何其他事情,我们也不需要在工厂模拟中添加任何逻辑。只要它就在那里就足够了。

因此,我们可以正确测试控制器动作:

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

我仍然想知道为什么IDE没有准确/一致地呈现潜在的异常。这是一个错误,还是由于async / await运算符和NUnit Test Adapter / runner?

我在异步测试中看到过类似的东西,我无法正确调试它们或者异常无法正确显示。我不记得在Visual Studio和xUnit的最新版本中看到这一点(我个人使用的是xUnit,而不是NUnit)。如果有帮助,从dotnet test命令行运行测试通常会正常工作,您将获得正确的(异步)堆栈跟踪故障。

答案 1 :(得分:2)

  

这是与DI相关的问题(测试中是否需要提供但未正常执行的内容)?

您正在调用框架在运行时为您设置的功能。在隔离的单元测试期间,您需要自己设置它们。

Controller的HttpContext缺少用于解析IServiceProvider的{​​{1}}。该服务实际上是IAuthenticationService

为了让......

SignInAsync

...在单元测试期间执行完成的await HttpContext.SignInAsync(principal); // FAILS HERE 操作中,您需要模拟服务提供者,以便Registration扩展方法不会失败。

更新单元测试安排

SignInAsync

其中//...code removed for brevity auth.ControllerContext.HttpContext = new DefaultHttpContext() { RequestServices = createServiceProviderMock() }; //...code removed for brevity 是用于模拟将用于填充createServiceProviderMock()

的服务提供商的小方法
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; } ,以便对该控制器操作进行隔离单元测试,以确保它在没有任何负面影响的情况下流向完成

答案 2 :(得分:1)

@poke提到您最好不要在单元测试中使用依赖注入并显式提供依赖(使用模拟),但是,我在集成测试中遇到了这个问题,并且我发现问题是由RequestServices属性引起的HttpContext在测试中没有正确初始化(因为我们在测试中没有使用实际的HttpContext),所以我像下面一样注册了HttpContextAccessor并自己(手动)通过了所有必需的服务,问题得到解决。参见下面的代码

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

我同意这不是一个很干净的解决方案,但是请注意,我仅在测试中编写和使用了此代码,以便在您的应用程序IHttpContextAccessor中提供所需的HttContext依赖关系(测试方法中未自动提供)。 ,HttpContext及其所需的服务由框架自动提供。

这是我的测试基类构造函数中所有依赖项的注册方法

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