繁体   English   中英

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

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

我有一个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");
}

更新3:基于聊天 @nkosi 建议这是一个问题源于我没有满足HttpContext的依赖注入要求的需求。

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

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

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

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

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

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

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

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


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

SignInAsync是一种扩展方法, 基本上将执行以下操作

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

因此,为方便起见,此方法将使用服务定位器模式从依赖项注入容器中检索服务以执行登录。 因此,对HttpContext这一个方法调用将HttpContext进一步的隐式依赖关系,您只能在测试失败时发现这些依赖关系。 这应该是您应该避免服务定位器模式的一个很好的示例:构造函数中的显式依赖关系更易于管理。 - 但是在这里,这是一种方便的方法,所以我们将不得不忍受这一点,只需调整测试即可。

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

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

我们可以从代码中看到SignInAsync实际上做了什么,我们需要为HttpContext.RequestServices提供一个IServiceProvider ,并使其能够返回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后在ControllerContext传递该服务提供者:

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

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

不幸的是,还有更多。 正如我在另一个答案 (您已经发现)中所解释的那样,当您在单元测试中设置RequestServices时,从控制器返回RedirectToActionResult将导致问题。 由于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从命令行运行测试通常会正常工作,您将获得适当的(异步)堆栈跟踪故障。

这是DI相关的问题(测试中没有提供但是正常执行中需要的东西)?

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

Controller的HttpContext缺少一个用于解析IAuthenticationServiceIServiceProvider 该服务实际上称为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;
}

我还建议模拟Repository以便对该控制器操作进行隔离单元测试,以确保它流向完成而没有任何负面影响

正如@poke所提到的,你最好不要在单元测试中使用依赖注入并明确地提供依赖(使用模拟)但是,我在集成测试中遇到了这个问题,并且我认为问题来自HttpContext RequestServices属性,该属性未正确初始化测试(因为我们在测试中没有使用实际的HttpContext)所以我在下面注册了我的HttpContextAccessor并且自己(手动)传递了所有它所需的服务并解决了问题。 见下面的代码

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

我同意它不是一个非常干净的解决方案,但请注意我在我的测试中编写和使用此代码,以便在您的应用程序IHttpContextAccessorHttpContext及其所需服务中提供所需的HttContext依赖项(在测试方法中不会自动提供)。由框架自动提供。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM