Unit testing Autofac registrations
We use Autofac as our IoC container at Konstrukt, so we can easily mock dependencies when unit testing, allow for more flexibility and general decoupling. All the normal stuff you would expect with IoC and dependency injection. Fore the most part, I’ve been very pleased with Autofac, and I’m a big fan of Dependency Injection and Inversion of Control. But, in a test-driven environment, where we mock out the dependencies further down the tree, we often get scenarios where we forget to register the types and interfaces in our modules (and container), only to be surprised by runtime errors and exceptions. Registering all classes with matching interfaces in dependent assemblies in an automated manner is not an option, as the interfaces aren’t always header interfaces, and we don’t want to add more registrations than we need, and a few other reasons.
There might be a better way to avoid this, than the method I’m describing below, but this is what I’ve done to verify the registrations are correct. I created a test template for unit testing Autofac modules, and the tests in the template try to resolve all the registered types. You can also exclude types and assemblies, something I’ve had to do with lambda registrations that contain logic which cannot be automagically resolved.
Important note: The tests only test that the registered types in a given module can be resolved, and does not test registrations done outside of the module. This can easily be changed by combining modules or passing in a modules list.
Scenarios that will return red tests:
Type is does not implementing registered interface
- Example: User is registered with IUser, but User does not implement IUser
Type is not registered
- Example: User expects IAccessTree in the constructor, but type isn’t registered
Type expects type that cannot be resolved
- Example: Same as above
This is the setup, let me know what you think- and I hope somebody else finds this useful. It has been extremely useful for us, and we’ve picked up many potential deal breakers.
The template creates a folder with the Given_Something naming convention, containing and Arrange.cs file, and a When_Resolving_All.cs file. There are templates for adding additional tests. The example below is just an example, but based on real code. Test.Common is a separate assembly.
<img class="aligncenter size-full wp-image-38483" src="https://inlovewithcode.azureedge.net/wp-content/uploads/2019/09/unit-testing-autofac-registrations2.png" alt="unit testing autofac registrations2" width="429" height="343" />
Arrange.cs inherits from ContainerArrangeBase.cs which does the underlying plumbing/setup expecting a type that implements IModule.
public abstract class ContainerArrangeBase<TModule> where TModule : IModule, new()
{
protected IContainer Container { get; private set; }
protected abstract List<IModule> Overrides { get;}
[SetUp]
public void TestBaseSetup()
{
var builder = new ContainerBuilder();
builder.RegisterModule<TModule>();
foreach (var moduleOverrides in Overrides)
{
builder.RegisterModule(moduleOverrides);
}
Container = builder.Build();
}
}
Notice that classes that inherit from this class have to define overrides. This can be an empty list, but generally we always have types we need to override.
Back to Arrange.cs:
public class Arrange : ContainerArrangeBase<ImportantModule>
{
protected virtual List<string> IgnoredAssemblies => new List<string>() {"System.Object"};
protected override List<IModule> Overrides => new List<IModule>() { new OverridesModule() };
protected ILifetimeScope Scope;
[SetUp]
public void Setup() => Scope = Container.BeginLifetimeScope();
}
The IgnoredAssemblies can be overridden by the test class, and those are ignored assemblies and types. I probably should move the ownership of the properties, now that I think about it.
public class When_Resolving_All_Services : Arrange
{
[SetUp]
public void Act()
{
// Do other stuff
}
[Test]
public void Should_Not_Throw() => Assert.DoesNotThrow(() => Scope.ResolveAll(IgnoredAssemblies));
}
This is the module for overrides, it’s a module as we need to override these types in several tests as they depend on logic that cannot be automatically resolved.
public class OverridesModule : Module
{
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
builder.Register(c => new Mock<KonstruktEntities>().Object);
}
}
If a registration is missing, the test will fail, yielding an exception that can help us locate the missing type. The magic, the ResolveAll() method, is an extension method that looks like this:
public static class ScopeExtensions
{
public static IList<IServiceWithType> Filter(this IEnumerable<IServiceWithType> services,
IEnumerable<string> ignoredAssemblies)
{
return services.Where(serviceWithType => ignoredAssemblies
.All(ignored => ignored != serviceWithType.ServiceType.FullName)).ToList();
}
public static IList<object> ResolveAll(this ILifetimeScope scope, IEnumerable<string> ignoredAssemblies)
{
var services = scope.ComponentRegistry.Registrations.SelectMany(x => x.Services)
.OfType<IServiceWithType>().Filter(ignoredAssemblies).ToList();
foreach (var serviceWithType in services)
{
try
{
scope.Resolve(serviceWithType.ServiceType);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
return services.Select(x => x.ServiceType).Select(scope.Resolve).ToList();
}
}
Hope this helps! Happy for any feedback- as always, I LOVE code review :D
Comments
Last modified on 2019-09-07