尚未注册类型“Microsoft.AspNetCore.Mvc...”的服务

geo*_*rtz 6 c# unit-testing asp.net-core-mvc asp.net-core

我正在尝试测试此控制器方法,以确保它重定向到另一个控制器方法或存在模型错误。

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);
}
Run Code Online (Sandbox Code Playgroud)

当我运行测试时,我收到两条不同的错误消息。当它尝试 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)
Run Code Online (Sandbox Code Playgroud)

当它尝试返回视图时,错误消息是...

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)
Run Code Online (Sandbox Code Playgroud)

我的 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?}");
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

测试

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);
}
Run Code Online (Sandbox Code Playgroud)

pok*_*oke 4

您正在对您的UserController。因此,当然,当控制器想要从服务提供商处解析某些内容时,如果您尚未为其设置模拟,那么该操作将会失败。

\n\n

如果您编写这样的测试,那么您的 Startup 中的任何代码当然都不会运行。单元测试应该控制依赖项,因此在启动中注册一堆实际上与测试用例无关的依赖项是没有意义的。

\n\n

可以使用测试服务器运行整个应用程序。这是一个集成测试,此时,您通常根本不应该使用模拟。

\n\n
\n\n

不管怎样,让\xe2\x80\x99s实际看看你的单元测试,看看\xe2\x80\x99s这里发生了什么。

\n\n

在第一种情况下,您会收到以下错误消息:

\n\n
\n

System.InvalidOperationException:尚未注册类型“Microsoft.AspNetCore.Mvc.Routing.IUrlHelperFactory”的服务。

\n
\n\n

因此,这里RedirectToAction的实现尝试从服务提供者ControllerBase检索。IUrlHelperFactory这样做的原因是因为RedirectToActionResult正在创建的对象需要一个UrlHelper

\n\n

在第二种情况下,您会收到以下错误:

\n\n
\n

System.InvalidOperationException:尚未注册类型“Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionaryFactory”的服务。

\n
\n\n

View这是由类似地创建传递的方法ViewResult引起的TempData。为了检索TempData它需要一个ITempDataDictionaryFactory从服务提供商处解析它。

\n\n
\n\n

所以基本上,如果您想像与活跃的服务提供商一样运行测试,您也必须提供这些服务。然而,控制器的构建方式,您也可以简单地跳过服务提供者。

\n\n

我不\xe2\x80\x99不知道为什么你在测试中需要身份验证服务:如果你正在测试的操作需要它,那么你应该将它作为控制器的实际依赖项。如果您在操作中使用HttpContext.RequestServices.GetService,那么您应该重新考虑\xe2\x80\x99s服务定位器模式,当您进行依赖注入时通常应该避免这种模式避免这种模式。

\n\n

如果您将身份验证服务设置为用户控制器的实际依赖项,则可以直接在构造函数中传递它。然后你就不会使用服务提供者,因此控制器不会尝试解决上面的这些依赖关系,并且不会出现错误。

\n