t.j*_*.j. 14 c# nunit-3.0 asp.net-core-2.0
我有一个ASP.Net Core 2.0 Web应用程序我正在使用单元测试进行改造(使用NUnit).该应用程序工作正常,迄今为止大多数测试工作正常.
但是,测试身份验证/授权(用户是否已登录并可以访问已[Authorize]过滤的操作)失败...
System.ArgumentNullException: Value cannot be null.
Parameter name: provider
Run Code Online (Sandbox Code Playgroud)
...后...
await HttpContext.SignInAsync(principal);
Run Code Online (Sandbox Code Playgroud)
......但目前尚不清楚其根本原因是什么.代码执行在此处停止,并且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();
}
Run Code Online (Sandbox Code Playgroud)
失败的测试代码......
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");
}
Run Code Online (Sandbox Code Playgroud)
UPDATE3:基于聊天 @nkosi 建议这是一个问题源于我没有满足依赖注入要求的需要HttpContext.
但是,尚不清楚的是:如果它实际上是一个不提供正确服务依赖性的问题,为什么代码正常工作(未经测试时).SUT(控制器)只接受一个IRepository参数(所以在任何情况下都提供了所有这些参数.)为什么创建一个重载的ctor(或mock)只是为了测试,当现有的ctor是在运行程序时调用的所有ctor和它运行没有问题?
更新4:虽然@Nkosi用解决方案回答了错误/问题,但我仍然想知道为什么IDE没有准确/一致地呈现基础异常.这是一个错误,还是由于async/await运算符和NUnit Test Adapter/runner?为什么在调试测试时出现异常"弹出",退出代码仍为零(通常表示成功返回状态)?
pok*_*oke 14
目前尚不清楚的是:如果它实际上是一个不提供正确服务依赖性的问题,为什么代码正常工作(未经测试时).SUT(控制器)只接受一个
IRepository参数(所以在任何情况下都提供了所有参数.)为什么创建一个重载的ctor(或mock)只是为了测试,当现有的ctor是在运行程序时调用的所有ctor运行没有问题?
你在这里混合了一些东西:首先,你不需要创建单独的构造函数.不用于测试,也不用于实际运行它作为应用程序的一部分.
您应该将控制器具有的所有直接依赖项定义为构造函数的参数,以便当它作为应用程序的一部分运行时,依赖项注入容器将向控制器提供这些依赖项.
但这也是重要的一点:运行应用程序时,有一个依赖注入容器,负责创建对象并提供所需的依赖项.所以你实际上并不需要过多担心它们来自哪里.但是在单元测试时这是不同的.在单元测试中,我们不想使用依赖注入,因为这只会隐藏依赖项,因此可能会产生可能与我们的测试冲突的副作用.依赖于单元测试中的依赖注入是一个非常好的迹象,表明您不是单元测试而是进行集成测试(至少除非您实际测试DI容器).
相反,在单元测试中,我们希望显式创建明确提供所有依赖项的所有对象.这意味着我们新建控制器并传递控制器具有的所有依赖项.理想情况下,我们使用模拟,因此我们不依赖于单元测试中的外部行为.
这大部分时间都很直接.不幸的是,控制器有一些特殊之处:控制器具有ControllerContext在MVC生命周期中自动提供的属性.MVC中的一些其他组件具有类似的东西(例如,ViewContext也自动提供).这些属性不是构造函数注入的,因此依赖项不是显式可见的.根据控制器的作用,在单元测试控制器时,您可能还需要设置这些属性.
进入你的单元测试,你正在使用HttpContext.SignInAsync(principal)你的控制器动作,所以不幸的是,你正在使用HttpContext直接操作.
SignInAsync是一种扩展方法,基本上将执行以下操作:
context.RequestServices.GetRequiredService<IAuthenticationService>().SignInAsync(context, scheme, principal, properties);
Run Code Online (Sandbox Code Playgroud)
因此,为方便起见,此方法将使用服务定位器模式从依赖项注入容器中检索服务以执行登录.因此,只需要调用一个方法HttpContext就可以获得进一步的隐式依赖关系,这些依赖关系只能在测试失败时发现.这应该是您应该避免服务定位器模式的一个很好的示例:构造函数中的显式依赖关系更易于管理. - 但是在这里,这是一种方便的方法,所以我们将不得不忍受这一点,只需调整测试即可.
实际上,在继续之前,我想在这里提一个好的替代解决方案:由于控制器是一个AuthController我只能想象它的核心目的之一是进行身份验证,签署用户进出事物.因此,不使用它实际上是一个好主意,HttpContext.SignInAsync而是将其IAuthenticationService作为对控制器的显式依赖,并直接调用其上的方法.这样,您就可以在测试中实现明确的依赖关系,而无需参与服务定位器.
当然,这将是这个控制器的特殊情况,并且不适用于扩展方法的每个可能的调用HttpContext.那么让我们来解决我们如何正确测试这个问题:
正如我们从代码中可以看到SignInAsync实际做的那样,我们需要提供一个IServiceProviderfor HttpContext.RequestServices和make,它能够返回一个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);
Run Code Online (Sandbox Code Playgroud)
然后,我们可以ControllerContext在创建控制器后传递该服务提供者:
var controller = new AuthController();
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext()
{
RequestServices = serviceProviderMock.Object
}
};
Run Code Online (Sandbox Code Playgroud)
这就是我们做HttpContext.SignInAsync工作所需要做的一切.
不幸的是,还有更多.正如我在其他答案中所解释的那样(您已经找到),RedirectToActionResult当您RequestServices在单元测试中进行设置时,从控制器返回a 将导致问题.由于RequestServices不为null,因此实现RedirectToAction将尝试解析a IUrlHelperFactory,并且该结果必须为非null.因此,我们需要扩展我们的模拟以提供一个:
var urlHelperFactory = new Mock<IUrlHelperFactory>();
serviceProviderMock
.Setup(s => s.GetService(typeof(IUrlHelperFactory)))
.Returns(urlHelperFactory.Object);
Run Code Online (Sandbox Code Playgroud)
幸运的是,我们不需要做任何其他事情,我们也不需要为工厂模拟添加任何逻辑.只要它就在那里就足够了.
因此,我们可以正确测试控制器操作:
// 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);
Run Code Online (Sandbox Code Playgroud)
我仍然想知道为什么IDE没有准确/一致地呈现基础异常.这是一个错误,还是由于async/await运算符和NUnit Test Adapter/runner?
我在异步测试中看到过类似的东西,我无法正确调试它们或者异常无法正确显示.我不记得在Visual Studio和xUnit的最新版本中看到这一点(我个人使用的是xUnit,而不是NUnit).如果它有帮助,从命令行运行测试dotnet test通常会正常工作,您将获得适当的(异步)堆栈跟踪故障.
| 归档时间: |
|
| 查看次数: |
2720 次 |
| 最近记录: |