No service for type 'Microsoft.AspNetCore.Mvc..." has been registered

10,787

Solution 1

You are unit-testing your UserController with only mock dependencies. So of course, when the controller wants to resolve something from the service provider, that will fail when you haven’t set up the mock for it.

If you write tests like that, of course none of the code from your Startup will run. Unit tests are supposed to keep the dependencies under control so it makes no sense to register a bunch of dependencies in your startup that are actually unrelated to your test case.

You can run your whole application using a test server. That is an integration test then, and at that point, you generally shouldn’t use mocks at all.


Anyway, let’s actually look at your unit test to see what’s going on here.

In the first case, you get the following error message:

System.InvalidOperationException : No service for type 'Microsoft.AspNetCore.Mvc.Routing.IUrlHelperFactory' has been registered.

So the RedirectToAction implementation of ControllerBase attempts to retrieve an IUrlHelperFactory from the service provider here. The reason it does that is because the RedirectToActionResult that is being created gets needs an UrlHelper passed.

In the second case, you get the following error:

System.InvalidOperationException : No service for type 'Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionaryFactory' has been registered.

This is caused by the View method which similarly creates a ViewResult with the TempData passed on. In order to retrieve the TempData it needs an ITempDataDictionaryFactory which it resolves from the service provider.


So basically, if you want to run your tests like that with an active service provider, you will have to provide those services too. However, the way the controller is built, you could also simply skip the service provider.

I don’t know why you need the authentication service in your test: If the action you are testing requires it, then you should have it as an actual dependency of your controller. If you are using HttpContext.RequestServices.GetService within your action, then you should rethink that as that’s the service locator pattern which you should generally avoid when you have dependency injection.

If you make the authentication service an actual dependency of your user controller, you would pass it directly in the constructor. And then you would not choose to use a service provider, so the controller would not attempt to resolve those dependencies above and there would be no errors.

Solution 2

To get a mocked ControllerContext for unit testing i did this (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 };

Using this as ControllerContext when creating a controller instance for testing solves the problem and makes it possible to mock other parts of the context such as session data.

Share:
10,787
geoff swartz
Author by

geoff swartz

Updated on June 26, 2022

Comments

  • geoff swartz
    geoff swartz almost 2 years

    I'm trying to test this controller method to make sure that it redirects to another controller method or has a model error.

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

    When I run my tests I get two different error messages. When it is trying to RedirectToAction I get the error...

    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)
    

    When it tries to return a view the error message is...

    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)
    

    I have services.AddMvc() in my Startup class however I put a breakpoint there and when I debug the test it doesn't hit the breakpoint. So I'm not sure if it's loading that or if debugging is just not picking that up. I've also added the Microsoft.AspNetCore.Mvc.ViewFeatures nuget package to my test project hoping maybe that was part of it, but no luck.

    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?}");
            });
        }
    }
    

    Tests

    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 = "[email protected]",
            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 = "[email protected]",
            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);
    }
    
  • geoff swartz
    geoff swartz about 6 years
    Ah, ok. Still a little unclear, but I think I get the jist of it. The mocking of the auth service and service provider was due to the controller having a method that logged in a user, calling the HttpContext.SignInAsync(principal) method. In trying to figure out how to make that work, I found the mocking code for the auth service and service provider, not realizing the side effect that was having. Thanks.
  • t.j.
    t.j. about 6 years
    @poke - could you provide some additional examples as to how to test a controller action which returns RedirectToAction... without including the UrlHelper and MVC services? The MS docs/examples don't seem to need it either and I am not sure how that is.
  • poke
    poke about 6 years
    @t.j. Basically, just make sure that you do not provide the IServiceProvider in the HttpContext that you are using. Then, the paths that would request services from the DI container will be just skipped. Doing this will of course prevent you from actually executing the redirect result but since you are testing the controller, that doesn’t matter and the result that you do get back is enough to verify the controller result in a unit test.
  • t.j.
    t.j. about 6 years
    @poke - Ah! Therein lies my challenge. I am newing up a ControllerContext, and adding a mock auth service to the RequestedServices (which, from your comments, is what is triggering the issue with UrlHelper/MVC.) I suppose the answer is to inject a UserManager rather than moq'ing the auth service?
  • t.j.
    t.j. about 6 years
  • poke
    poke about 6 years
    @t.j. Answered over there.