'How to test that all required services were registered with DI service collection

In the Startup file of ASP.NET Core application, we register hundreds of different services.
During refactoring some services were moved to the dedicated projects and their registrations were wrapped with an extension methods for IServiceCollection.
Some services were grouped by domain logic and dependency tree and their registration were moved to the extension methods as well.

I was thinking is there are any way to test that all required services are registered within DI?

Since this is a Web application and all application entry points are controllers. It could be possible just to instantiate all controllers with service collection after executing Startup.ConfigureServices, but I didn't get it work in unit tests.

As a test result I would be happy to be sure that all controllers were instantiate successfully.



Solution 1:[1]

I faced the same problem and I could manage it by the hint of Chris Pratt

I wrote an extension method for the IServiceProvider:



/// <summary>
/// Don't use that to get useable Instance. The returning instance is maybe broken
/// </summary>
/// <param name="services">IServiceProvider</param>
/// <param name="controllerType">Type for controller instance</param>
/// <returns>Broken instance of an controller</returns>
public static object GetController(this IServiceProvider services, Type controllerType)
{
    var controllerTypeInfo = controllerType.GetTypeInfo();

    // don't care about which function, just filling the descriptor to get the right Controller
    var methodName = controllerTypeInfo.GetMethods()[0].Name;


    var actionDescriptor = new ControllerActionDescriptor()
    {
        ControllerTypeInfo = controllerTypeInfo,
        ControllerName = controllerTypeInfo.Name,
        MethodInfo = controllerTypeInfo.GetMethod(methodName),
        ActionName = methodName
    };

    var modelStateDictionary = new ModelStateDictionary();
    var mockHttpContext = new Mock<HttpContext>();
    mockHttpContext.Setup(m => m.RequestServices).Returns(services);

    var actionContext = new ActionContext();
    var controllerContext = new ControllerContext()
    {
        HttpContext = mockHttpContext.Object,
        ActionDescriptor = actionDescriptor,
        ValueProviderFactories = new List<IValueProviderFactory>(),
    };

    var activator = services.GetRequiredService<IControllerActivator>();
    var controller = activator.Create(controllerContext);

    return controller;
}

And the usage in the test:

[Test]
public void ServiceCanBuild()
{
    var builder = Program.CreateHostBuilder(new string[] { });
    IHost host = builder.ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    }).Build();

    string nspace = "NameSpace.Of.Controller";
    typeof(Program).Assembly.GetExportedTypes()
        .Where(t => t.IsClass && t.Namespace.Equals(nspace, StringComparison.OrdinalIgnoreCase))
        .Select(host.Services.GetController)
        .ToList();

    Assert.Pass();
}

Solution 2:[2]

Haven't tried this, but basically, you need to call your service registration, and then get a controller out of that. If necessary services required to activate the controller are missing, then it should throw an InvalidOperationException, which you can test for.

However, by default, controllers themselves are not actually registered in the service collection. There's a way to do so, but using that just to enable testing is a bad idea. Instead you should merely rely on IControllerActivator:

var services = // call method that registers services;
var provider = services.Build();

var activator = provider.GetRequiredService<IControllerActivator>();
var context = new ControllerContext { ... };
var controller = activator.Create(context);

// assert something useful

ControllerContext is what tells the activator what controller to instantiate and what action method to call. As such, you'll need to mock this out with the info you need for each particular controller.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 peni4142
Solution 2 Chris Pratt