c# - ASP.Net Core 2.0 SignInAsync返回异常值不能为null,提供程序

标签 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的依赖项注入(inject)需求。

However,目前尚不清楚:如果实际上这是未提供适当的服务依赖关系的问题,那么为什么代码正常工作(当未经测试时)。 SUT( Controller )仅接受IRepository参数(因此无论如何都将提供此参数。)为什么要创建一个过载的ctor(或模拟)以用于测试,而在运行程序时仅调用现有的ctor,并且它运行没有问题?

UPDATE4 :@Nkosi用解决方案回答了错误/问题时,我仍然想知道为什么IDE无法准确/一致地呈现底层异常。这是一个错误,还是由于异步/等待操作符和NUnit测试适配器/运行器?为什么异常不会像调试测试时所期望的那样“突然弹出”,并且退出代码仍为零(通常表示成功的返回状态)?

最佳答案

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?



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

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

但这也很重要:在运行应用程序时,有一个依赖项注入(inject)容器,负责创建对象并提供所需的依赖项。因此,您实际上不必担心它们的来源。但是,在进行单元测试时,这是不同的。在单元测试中,我们不希望使用依赖项注入(inject),因为这只会隐藏依赖项,因此可能会与我们的测试产生冲突。在单元测试中依赖依赖注入(inject)是一个很好的信号,表明您不是单元测试,而是进行了集成测试(至少除非您实际上在测试DI容器)。

相反,在单元测试中,我们要显式创建所有对象,以提供所有显式依赖。这意味着我们将更新 Controller 并传递 Controller 具有的所有依赖关系。理想情况下,我们使用模拟,因此我们在单元测试中不依赖外部行为。

在大多数情况下,这都是非常简单的。不幸的是, Controller 有一些特殊之处: Controller 具有ControllerContext属性,该属性在MVC生命周期中自动提供。 MVC中的其他一些组件也有类似的功能(例如ViewContext也自动提供)。这些属性不是构造函数注入(inject)的,因此依赖项不是显式可见的。根据 Controller 的功能,在对 Controller 进行单元测试时,可能还需要设置这些属性。

进行单元测试时,您正在 Controller Action 中使用HttpContext.SignInAsync(principal),因此很遗憾,您直接使用HttpContext进行操作。
SignInAsyncwill basically do the following的扩展方法:
context.RequestServices.GetRequiredService<IAuthenticationService>().SignInAsync(context, scheme, principal, properties);

因此,为纯粹方便起见,此方法将使用service locator pattern从依赖项注入(inject)容器中检索服务以执行登录。因此,仅对HttpContext的这一方法调用将拉入您仅在测试失败时才发现的其他隐式依赖项。这应该作为why you should avoid the service locator pattern的一个很好的例子:构造函数中的显式依赖项更易于管理。 –但是在这里,这是一种便捷的方法,因此我们将不得不接受这种方法,而只是调整测试以使其适用。

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

当然,这将是此 Controller 的一种特殊情况,并且不适用于HttpContext上所有可能的扩展方法调用。因此,让我们解决如何正确测试它:

从代码中可以看到SignInAsync实际执行的操作,我们需要为IServiceProvider提供HttpContext.RequestServices并使其能够返回IAuthenticationService。因此,我们将 mock 这些:
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);

然后,我们可以在创建 Controller 后在ControllerContext中传递该服务提供者:
var controller = new AuthController();
controller.ControllerContext = new ControllerContext
{
    HttpContext = new DefaultHttpContext()
    {
        RequestServices = serviceProviderMock.Object
    }
};

我们要做的就是使HttpContext.SignInAsync正常工作。

不幸的是,还有更多的东西。正如我在this other answer(您已经找到的)中所解释的那样,当您在单元测试中设置了RedirectToActionResult时,从 Controller 返回RequestServices会导致问题。由于RequestServices不为null,因此RedirectToAction的实现将尝试解析IUrlHelperFactory,并且该结果必须为非null。因此,我们需要稍微扩展一下模拟以提供该模拟:
var urlHelperFactory = new Mock<IUrlHelperFactory>();
serviceProviderMock
    .Setup(s => s.GetService(typeof(IUrlHelperFactory)))
    .Returns(urlHelperFactory.Object);

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

因此,我们可以正确测试 Controller 的 Action :
// 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?



过去,我在异步测试中也看到过类似的情况,即无法正确调试它们,或者无法正确显示异常。我不记得在Visual Studio和xUnit的最新版本中看到过这种情况(我个人使用的是xUnit,而不是NUnit)。如果有帮助,通常使用dotnet test从命令行运行测试通常可以正常工作,并且您将获得正确的(异步)堆栈跟踪以了解失败。

关于c# - ASP.Net Core 2.0 SignInAsync返回异常值不能为null,提供程序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48939674/

相关文章:

c# - 通缉 : C# programming library for mathematics

c# - 强制客户端从 ASP 中的特定入口点进入站点

nunit - TeamCity XML 报告处理不适用于 NUnit 3 报告文件

c# - 无法将.Net Core 2.0项目的项目引用添加到Azure函数项目(netStandard2.0)

docker - Identityserver4 openid-configuration 省略运行 nginx 反向代理的主机端口

unit-testing - 如何使用 NUnit 3 在 Atlassian Bamboo 中运行 NUnit Runner?

nunit-3.0 - NUnit3TestExecutor转换了279个NUnit测试用例中的279个

c# - WCF 访问被拒绝异常

c# - C# 程序员的 COBOL Copybook 规范

c# - ASP.NET Core 2 - 多个 Azure Redis 缓存服务 DI