我正在尝试测试此 Controller 方法以确保它重定向到另一个 Controller 方法或有模型错误。
public IActionResult ResetPassword(ResetPasswordViewModel viewModel)
{
if (viewModel.NewPassword.Equals(viewModel.NewPasswordConfirm))
{
...do stuff
return RedirectToAction("Index", "Home");
}
ModelState.AddModelError("ResetError", "Your passwords did not match. Please try again");
return View(viewModel);
}
当我运行测试时,我收到两条不同的错误消息。当它尝试 RedirectToAction 时出现错误...
System.InvalidOperationException : No service for type 'Microsoft.AspNetCore.Mvc.Routing.IUrlHelperFactory' has been registered.
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
at Microsoft.AspNetCore.Mvc.ControllerBase.get_Url()
at Microsoft.AspNetCore.Mvc.ControllerBase.RedirectToAction(String actionName, String controllerName, Object routeValues, String fragment)
at Microsoft.AspNetCore.Mvc.ControllerBase.RedirectToAction(String actionName, String controllerName, Object routeValues)
at Microsoft.AspNetCore.Mvc.ControllerBase.RedirectToAction(String actionName, String controllerName)
当它试图返回一个 View 时,错误信息是...
System.InvalidOperationException : No service for type 'Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionaryFactory' has been registered.
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
at Microsoft.AspNetCore.Mvc.Controller.get_TempData()
at Microsoft.AspNetCore.Mvc.Controller.View(String viewName, Object model)
at Microsoft.AspNetCore.Mvc.Controller.View(Object model)
我的 Startup 类中有 services.AddMvc(),但是我在那里放置了一个断点,当我调试测试时它没有到达断点。所以我不确定它是否正在加载,或者调试是否只是没有接受它。我还将 Microsoft.AspNetCore.Mvc.ViewFeatures nuget 包添加到我的测试项目中,希望这可能是其中的一部分,但没有运气。
启动.cs
public class Startup
{
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", true, true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfiguration Configuration { get; protected set; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
SetupDatasources(services);
services.AddWebEncoders();
services.AddMvc();
services.AddScoped<IEmailRepository, EmailRepository>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "PSC";
options.LoginPath = "/Home/Index";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.LogoutPath = "/User/LogOut";
});
}
public virtual void SetupDatasources(IServiceCollection services)
{
services.AddDbContext<EmailRouterContext>(opt =>
opt.UseSqlServer(Configuration.GetConnectionString("EmailRouter")));
services.AddDbContext<PSCContext>(opt =>
opt.UseSqlServer(Configuration.GetConnectionString("PSC")));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
app.UseAuthentication();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseEmailingExceptionHandling();
}
else
{
app.UseStatusCodePages();
app.UseEmailingExceptionHandling();
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
"default",
"{controller=Home}/{action=Index}/{id?}");
});
}
}
测试
private Mock<IEmailRepository> emailRepository;
private Mock<IUserRepository> userRepository;
private UserController controller;
private Mock<HttpContext> context;
private Mock<HttpRequest> request;
[SetUp]
public void Setup()
{
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));
var serviceProviderMock = new Mock<IServiceProvider>();
serviceProviderMock
.Setup(_ => _.GetService(typeof(IAuthenticationService)))
.Returns(authServiceMock.Object);
emailRepository = new Mock<IEmailRepository>();
userRepository = new Mock<IUserRepository>();
context = new Mock<HttpContext>();
context.Setup(x => x.RequestServices).Returns(serviceProviderMock.Object);
request = new Mock<HttpRequest>(MockBehavior.Loose);
request.Setup(x => x.Scheme).Returns("https");
request.Setup(x => x.Host).Returns(new HostString("www.oursite.com", 80));
context.Setup(x => x.Request).Returns(request.Object);
controller = new UserController(userRepository.Object, emailRepository.Object)
{
ControllerContext = new ControllerContext
{
HttpContext = context.Object
}
};
}
[Category("ResetUserPassword")]
[Test]
public void ResetPassword_should_save_new_password()
{
var viewModel = new ResetPasswordViewModel()
{
Token = "abc12",
Email = "user@oursite.com",
NewPassword = "123123",
NewPasswordConfirm = "123123",
Used = false
};
userRepository.Setup(x => x.SaveNewPassword(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()));
userRepository.Setup(x => x.SaveUsedToken(It.IsAny<string>(), It.IsAny<string>()));
userRepository.Setup(x => x.ValidateLogin(It.IsAny<UserLogin>())).Returns(new User()
{
EmailAddress = viewModel.Email, UserTypeId = UserType.FriendFamily.Value
});
var result = controller.ResetUserPassword(viewModel);
userRepository.Verify(x => x.SaveNewPassword(viewModel.Email, It.IsAny<string>(), It.IsAny<string>()), Times.Once);
userRepository.Verify(x => x.SaveUsedToken(viewModel.Token, viewModel.Email));
Assert.IsInstanceOf<ViewResult>(result);
}
[Category("ResetUserPassword")]
[Test]
public void ResetPassword_should_have_error_count_greater_than_0()
{
var viewModel = new ResetPasswordViewModel()
{
Token = "abc12",
Email = "user@oursite.com",
NewPassword = "123123",
NewPasswordConfirm = "456456",
Used = false
};
userRepository.Setup(x => x.SaveNewPassword(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()));
userRepository.Setup(x => x.SaveUsedToken(It.IsAny<string>(), It.IsAny<string>()));
controller.ResetUserPassword(viewModel);
userRepository.Verify(x => x.SaveNewPassword(viewModel.Email, It.IsAny<string>(), It.IsAny<string>()), Times.Once);
userRepository.Verify(x => x.SaveUsedToken(viewModel.Token, viewModel.Email));
Assert.IsTrue(controller.ModelState.ErrorCount > 0);
}
最佳答案
您正在对您的 UserController
进行单元测试,仅使用模拟依赖项。因此,当然,当 Controller 想要从服务提供者那里解决某些问题时,如果您没有为它设置模拟,那将会失败。
如果您编写这样的测试,当然您的 Startup 中的任何代码都不会运行。单元测试应该控制依赖关系,因此在您的启动中注册一堆实际上与您的测试用例无关的依赖关系是没有意义的。
您可以使用测试服务器运行整个应用程序。那是一个 integration test然后,到那时,您通常根本不应该使用模拟。
无论如何,让我们实际查看您的单元测试,看看这里发生了什么。
在第一种情况下,您会收到以下错误消息:
System.InvalidOperationException : No service for type 'Microsoft.AspNetCore.Mvc.Routing.IUrlHelperFactory' has been registered.
因此 ControllerBase
的 RedirectToAction
实现尝试从此处的服务提供商处检索 IUrlHelperFactory
。它这样做的原因是因为正在创建的 RedirectToActionResult
需要传递一个 UrlHelper
。
在第二种情况下,您会收到以下错误:
System.InvalidOperationException : No service for type 'Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionaryFactory' has been registered.
这是由 View
方法引起的,该方法类似地创建了一个 ViewResult
并传递了 TempData
。为了检索 TempData
,它需要一个从服务提供商解析的 ITempDataDictionaryFactory
。
所以基本上,如果您想通过活跃的服务提供商来运行您的测试,您也必须提供这些服务。但是,根据构建 Controller 的方式,您也可以跳过服务提供者。
我不知道你为什么在测试中需要身份验证服务:如果你正在测试的操作需要它,那么你应该将它作为 Controller 的实际依赖项。如果您在操作中使用 HttpContext.RequestServices.GetService
,那么您应该重新考虑一下,因为那是 service locator pattern你通常应该avoid when you have dependency injection .
如果您使身份验证服务成为您的用户 Controller 的实际依赖项,您将直接在构造函数中传递它。然后您将不选择使用服务提供者,因此 Controller 不会尝试解决上面的那些依赖关系并且不会有错误。
关于c# - 没有注册类型 'Microsoft.AspNetCore.Mvc..."的服务,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48873454/