没有服务类型' Microsoft.AspNetCore.Mvc ..."已注册

时间:2018-02-19 20:09:38

标签: c# unit-testing asp.net-core asp.net-core-mvc

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

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)

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

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包,希望可能是它的一部分,但没有运气。

Startup.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);
}

2 个答案:

答案 0 :(得分:2)

您只使用模拟依赖项对UserController进行单元测试。当然,当控制器想要从服务提供者处解决某些问题时,如果你没有为它设置模拟,这将失败。

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

可以使用测试服务器运行整个应用程序。那是integration test,那时你通常不应该使用模拟。

无论如何,让我们实际看一下你的单元测试,看看这里发生了什么。

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

  

System.InvalidOperationException:没有服务类型&#39; Microsoft.AspNetCore.Mvc.Routing.IUrlHelperFactory&#39;已经注册。

因此,RedirectToAction ControllerBase实施尝试从服务提供商处检索IUrlHelperFactory。这样做的原因是因为正在创建的RedirectToActionResult需要传递UrlHelper

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

  

System.InvalidOperationException:没有类型的服务&#39; Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionaryFactory&#39;已经注册。

这是由View方法引起的,该方法类似地创建了传递ViewResult的{​​{1}}。要检索TempData,它需要从服务提供商处解析的TempData

所以基本上,如果你想用活跃的服务提供商运行你的测试,你也必须提供这些服务。但是,构建控制器的方式,您也可以跳过服务提供商。

我不知道为什么在测试中需要身份验证服务:如果您正在测试的操作需要它,那么您应该将它作为控制器的实际依赖项。如果您在操作中使用ITempDataDictionaryFactory,那么您应该重新考虑,因为service locator pattern通常是avoid when you have dependency injection

如果使身份验证服务成为用户控制器的实际依赖关系,则可以直接在构造函数中传递它。然后你选择使用服务提供商,因此控制器不会尝试解决上面的那些依赖关系而且没有错误。

答案 1 :(得分:1)

为了获得用于单元测试的模拟 ControllerContext,我这样做了(NSubstitute):

    var httpContext = Substitute.For<HttpContext>();
    var session = Substitute.For<ISession>();
    httpContext.Session.Returns(session);
    var serviceProvider = Substitute.For<IServiceProvider>();
    serviceProvider
            .GetService(Arg.Is(typeof(ITempDataDictionaryFactory)))
            .Returns(Substitute.For<ITempDataDictionaryFactory>());
    serviceProvider
            .GetService(Arg.Is(typeof(IUrlHelperFactory)))
            .Returns(Substitute.For<IUrlHelperFactory>());
    httpContext.RequestServices.Returns(serviceProvider);
    var ctx = new ControllerContext { HttpContext = httpContext };

在创建用于测试的控制器实例时将其用作 ControllerContext 可以解决该问题,并且可以模拟上下文的其他部分,例如会话数据。