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,提供程序 的相关文章

  • #include 在 ubuntu 中“没有这样的文件”

    当使用 g std c 0x Wall test cc o hello 编译时 输出致命错误 cstdatomic 没有这样的文件或直接 哪里不见了 包含内容应该是 include
  • 如何指定CMAKE外部项目的编译器?

    我使用ExternalProject Add 包含一个外部项目 我想要的是能够做到 cmake DCMAKE CXX COMPILER
  • 使用 QTextCursor 选择一段文本

    使用 Qt 框架选择文本片段时遇到问题 例如 如果我有这个文件 没有时间休息 我想选择 ime for r 并从文档中删除这段文本 我应该如何使用 QTextCursor 来做到这一点 这是我的代码 QTextCursor cursor n
  • 将内核链接到 PTX 函数

    我可以使用 PTX 文件中包含的 PTX 函数作为外部设备函数 将其链接到另一个应调用该函数的 cu 文件吗 这是另一个问题CUDA 将内核链接在一起 https stackoverflow com questions 20636800 c
  • 在 DefaultHttpContext 上使用 FeatureCollection 时,响应对象为 null

    我正在测试一些 net Core 中间件 并希望使用整个 asp net Core http 管道来运行中间件 而不是模拟它 问题是 当我使用特征集合时 不知何故 响应对象没有在 httpRequest 中设置 并且它在请求本身上是只读的
  • 有哪些 API 可在 Windows 中使用 C# 配置扬声器设置?

    我环顾了很多不同的地方 但似乎找不到一个简单的方法来做到这一点 我在 Windows 7 中有多个声卡 并使用 HDMI 将声音输出到我的 AVR 放大器 我遇到的问题是 当放大器关闭时 它会导致窗口丢失扬声器配置 所以我想做的是编写一个小
  • 没有真正理解 std::atomic::compare_exchange_weak 和compare_exchange_strong 的逻辑

    我读了https en cppreference com w cpp atomic atomic compare exchange https en cppreference com w cpp atomic atomic compare
  • 让 GCC/Clang 使用 CMOV

    我有一个简单的标记值联合 这些值可以是int64 ts or doubles 我正在对这些联合进行加法 但需要注意的是 如果两个参数都代表int64 t值 那么结果也应该有一个int64 t value 这是代码 include
  • 最小对的总和

    Given 2N点 in a 2D plane 你必须将它们分组为N pairs使得所有对的点之间的距离的总和是最小可能值 所需的输出只是总和 换句话说 如果a1 a2 an分别是第一对 第二对 和第 n 对点之间的距离 则 a1 a2 a
  • 在标准库中静态链接时如何支持动态插件?

    假设一个应用程序myapp exe是使用构建的g 它使用标志 static libstdc 这样就可以安装在没有环境的情况下libstdc so myapp exe还添加了对某些功能的插件支持plugf可以通过动态加载dlopen来自共享库
  • 复杂的 C 声明

    我刚刚在互联网上浏览了一些代码 发现了这个 float foo SIZE SIZE 我如何阅读这份声明 是否有一套特定的规则来阅读如此复杂的声明 我有一段时间没做这个了 从 开始foo然后向右走 float foo SIZE SIZE fo
  • 表单.位置不起作用

    我之前问过这个问题 以为我已经解决了 但它仍然不起作用 Form Show 稍微移动表单位置 https stackoverflow com questions 1214014 form show moves form position s
  • DLL 中的 XP 风格组合框

    我需要使用 C 和 WIN32 API 无 MFC 在 DLL 中创建 XP 风格的组合框 我设法在 DLL 中创建控件 不是以 XP 风格 我设法在带有清单的 exe 中创建 XP 样式组合框 但它在 DLL 中不起作用 为了让您的 DL
  • 使用std::begin()、std::end()将ArrayXd转换为stl向量,

    在我看来我应该能够使用std begin and std end 转换ArrayXd to std vector
  • 序列化时如何跳过 xml 声明?

    我正在尝试输出一个没有 xml 头的 xml 文件 例如 我试过 Type t obj GetType XmlSerializer xs new XmlSerializer t XmlWriter xw XmlWriter Create c
  • RabbitMQ + Windows + LDAP 无需发送密码

    我正在尝试在 Windows 7 上使用 RabbitMQ 3 6 2 进行 LDAP 身份验证 授权 我已经在应用程序发送用户名 密码的情况下进行了基本身份验证 但密码位于我需要弄清楚如何进行的代码中避免 有没有人在不提供密码的情况下成功
  • 即使对于新上下文,OnModelCreating 也仅调用一次

    我有多个相同但内容不同的 SQL Server 表 在编写代码优先 EF6 程序时 我尝试为每个程序重用相同的数据库上下文 并将表名称传递给上下文构造函数 然而 虽然每次都会调用构造函数 但尽管每次都是从 new 创建数据库上下文 但 On
  • 没有运算符“<<”与这些操作数匹配[重复]

    这个问题在这里已经有答案了 不知道发生了什么事 我查看了与此问题类似的其他帖子 但到目前为止没有解决方案有帮助 这是带有错误部分注释的代码 在某一时刻 它说 不起作用 而在代码的其余部分中 它说 include
  • 通过 OCI 调用 Oracle 存储过程并使用 C++ 中的 out ref 游标返回结果

    我想使用 OCI 接口从 C 调用 Oracle 存储过程 并使用 out SYS REF CURSOR 作为过程的参数来迭代结果 我是 OCI 新手 所以可能会遗漏一些简单的东西 大部分代码取自这里 我的存储过程是 CREATE OR R
  • “保留供任何使用”是什么意思?

    注意 这是一个c questions tagged c问题 虽然我补充说c questions tagged c 2b 2b如果某些 C 专家可以提供 C 使用与 C 不同的措辞的基本原理或历史原因 在 C 标准库规范中 我们有这个规范文本

随机推荐