Search code examples
c#unit-testingnunitautofac

How do I set up this unit test to test some types explicitly?


I have a test fixture for my CompositionRoot static class that pretty much just enumerates my Autofac IContainer's services and attempts to instantiate them. If it can instantiate them, that's a test pass. The goal is to ensure I did not forget to register new interfaces with my Autofac container.

However, some types that are registered consume types in their constructors that are not intended to be, or cannot be, registered with Autofac. For example I have a class that takes a string in its constructor. The way I inject this class into my code base is:

Func<string, ITypeThatRequiresAFactory>

Here is my test fixture (using NUnit3 + FluentAssertions):

[TestFixture]
[Parallelizable(ParallelScope.All)]
public class CompositionRootTest
{
    private sealed class ConcreteTypeEnumerator : IEnumerable
    {
        private readonly IContainer _container;

        public ConcreteTypeEnumerator()
        {
            _container = CompositionRoot.Setup();
        }

        public IEnumerator GetEnumerator()
        {
            return _container.ComponentRegistry.Registrations
                .SelectMany(x => x.Services)
                .OfType<TypedService>()
                .GetEnumerator();
        }
    }

    [TestCaseSource(typeof(ConcreteTypeEnumerator))]
    public void Service_should_be_instantiable(Service service)
    {
        using var container = CompositionRoot.Setup();
        container.Invoking(c => c.ResolveService(service))
            .Should().NotThrow()
            .And.NotBeNull();
    }
}

The test Service_should_be_instantiable will fail when it tries to instantiate the service implementing ITypeThatRequiresAFactory because of that string parameter in its constructor.

How can I refactor my test fixture to:

  • Express a list of explicitly-tested types
  • Which are excluded from the list of types tested by Service_should_be_instantiable
  • And must have a manual test case written to verify they can be resolved (most likely using a special case composite type, like Func<>)?

The goal is to use Autofac's implicit relationship type for automatic factories (Func<>) and possibly other implicit relationships later (like Lazy<>), so a solution requiring me to explicitly register hand-written factories, for instance, is a non-starter.


Solution

  • What you'll need to do is:

    • Resolve types instead of services so you can exclude the types you want.
    • Remove duplicates (so if you have two things registered .As<IFoo>() you won't run that test twice).
    • Exclude Autofac types (because ILifetimeScope and IComponentContext are inherently registered and that shouldn't be part of your tests).

    That LINQ looks like:

    var exclude = new []
    {
      typeof(ThingToSkip),
      typeof(OtherThingToSkip),
    };
    
    // Get all the registrations
    // Pull out the services
    // Only look at typed (reflection) services
    // Get the actual type from the service
    // Filter down to unique types
    // Exclude the types you don't want to test
    // Exclude the Autofac types
    var typesToTest = container.ComponentRegistry.Registrations
      .SelectMany(x => x.Services)
      .OfType<TypedService>()
      .Select(t => t.ServiceType)
      .Distinct()
      .Except(exclude)
      .Where(t => !t.FullName.StartsWith("Autofac."));
    

    Your resolve line will change to:

    container.Resolve(type);
    

    ...because you'll resolve based on a type rather than a service.

    I think that should get you where you're going.

    Note this won't cover things like...

    • Open generics
    • IEnumerable<T>, Func<T>, or any of the other "built-in relationships" supported by Autofac
    • Anything registered as a lambda
    • Service location that might be done inside a constructor after resolving an ILifetimeScope or IComponentContext

    ...and so on. I mean, you probably got that because you're already filtering by TypedService but I figured I'd be explicit. That's the primary reason something like this isn't built into Autofac - folks would assume we have the ability to check all the registrations and that's not really possible.