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

2024-05-10

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

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

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

...后...

await HttpContext.SignInAsync(principal);

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

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

我正在努力创建一个复制品来向人们指出(到目前为止,结果有点复杂。)

有谁知道如何查明根本原因?这是一个与 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 https://chat.stackoverflow.com/rooms/165687/discussion-between-t-j-and-nkosi @nkosi 建议 https://chat.stackoverflow.com/transcript/message/41366788#41366788这是一个问题,源于我没有满足依赖注入的要求HttpContext.

However https://chat.stackoverflow.com/transcript/message/41367614#41367614,尚不清楚的是:如果实际上是未提供适当的服务依赖项的问题,那么为什么代码可以正常工作(在未测试时)。 SUT(控制器)只接受 IRepository 参数(因此这就是在任何情况下提供的全部内容。)为什么要创建一个重载的 ctor(或模拟)只是为了测试,当现有的 ctor 是运行程序时调用的全部并且它运行没有问题吗?

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


目前尚不清楚的是:如果实际上是未提供适当的服务依赖项的问题,那么为什么代码可以正常工作(在未测试时)。 SUT(控制器)仅接受IRepository参数(所以这就是在任何情况下提供的所有内容。)为什么要创建一个重载的 ctor(或模拟)只是为了测试,当现有的 ctor 是运行程序时调用的所有内容并且运行没有问题时?

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

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

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

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

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


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

SignInAsync是一种扩展方法,其中基本上会做以下事情 https://github.com/aspnet/HttpAbstractions/blob/rel/2.0.0/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationHttpContextExtensions.cs#L142:

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

因此,为了纯粹的方便起见,该方法将使用服务定位器模式 https://en.wikipedia.org/wiki/Service_locator_pattern从依赖项注入容器检索服务以执行登录。所以只有这一个方法调用HttpContext将进一步拉动implicit仅当测试失败时您才会发现的依赖项。这应该作为一个很好的例子为什么应该避免服务定位器模式 http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/:构造函数中的显式依赖项更易于管理。 – 但在这里,这是一种方便的方法,所以我们必须忍受它,并调整测试以适应这种情况。

实际上,在继续之前,我想在这里提到一个很好的替代解决方案:由于控制器是一个AuthController我只能想象它的核心目的之一是进行身份验证,登录和注销用户等等。所以不使用实际上可能是个好主意HttpContext.SignInAsync但相反有IAuthenticationService as an 显式依赖控制器上,并直接调用其上的方法。这样,您就可以在测试中实现明确的依赖关系,并且不需要涉及服务定位器。

当然,这对于该控制器来说是一个特殊情况,不适用于every可能调用扩展方法HttpContext。那么让我们来看看如何正确地测试它:

从代码中我们可以看出什么SignInAsync实际上,我们需要提供一个IServiceProvider for 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 work.

不幸的是,还有更多的事情要做。正如我在中所解释的这个另一个答案 https://stackoverflow.com/a/48875448/216074(你已经找到了),返回一个RedirectToActionResult当您有以下情况时,从控制器将导致问题RequestServices在单元测试中设置。自从RequestServices不为空,执行RedirectToAction将尝试解决IUrlHelperFactory,并且该结果必须是非空的。因此,我们需要稍微扩展我们的模拟以提供该模拟:

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 测试适配器/运行器造成的?

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

ASP.Net Core 2.0 SignInAsync 返回异常值不能为 null,提供程序 的相关文章

随机推荐