Search code examples
autofac

Modifying Autofac Scope to Support XUnit Testing


I use Autofac extensively. Recently I've gotten interested in tweaking the lifetime scopes when registering items for XUnit testing. Basically I want to register a number of standard components I use as "instance per test" rather than what I normally do for runtime (I've found a useful library on github that defines an instance-per-test lifetime).

One way to do this is to define two separate container builds, one for runtime and one for xunit testing. That would work but it gets increasingly expensive to maintain.

What I'd like to do (I think) is modify the registration pipeline dynamically depending upon the context -- runtime or xunit test -- in which it is being built. In pseudocode:

builder.RegisterType<SomeType>().AsImplementedInterfaces().SingleInstance();
...
void TweakPipeline(...)
{
    if( Testing )
    {
        TypeBeingRegistered.InstancePerTest();
    }
    else
    {
        TypeBeingRegistered.SingleInstance();
    }
}

Is this something Autofac middleware can do? If not is there another capability in the Autofac API which could address it? As always, links to examples would be appreciated.


Solution

  • This is an interesting question. I like that you started thinking about some of the new features in Autofac, very few do. So, kudos for the good question.

    If you think about the middleware, yes, you can probably use it to muck with lifetime scope, but we didn't really make "change the lifetime scope on the fly" something easy to do and... I'll be honest, I'm not sure how you'd do it.

    However, I think there are a couple of different options you have to make life easier. In the order in which I'd do them if it was me...

    Option 1: Container Per Test

    This is actually what I do for my tests. I don't share a container across multiple tests, I actually make building the container part of the test setup. For Xunit, that means I put it in the constructor of the test class.

    Why? A couple reasons:

    • State is a problem. I don't want test ordering or state on singletons in the container to make my tests fragile.
    • I want to test what I deploy. I don't want something to test out OK only to find that it worked because of something I set up in the container special for testing. Obvious exceptions for mocks and things to make the tests actually unit tests.

    If the problem is that the container takes too long to set up and is slowing the tests down, I'd probably troubleshoot that. I usually find the cause of this to be either that I'm assembly scanning and registering way, way too much (oops, forgot the Where statement to filter things down) or I've started trying to "multi-purpose" the container to start orchestrating my app startup logic by registering code to auto-execute on container build (which is easy to do... but don't forget the container isn't your app startup logic, so maybe separate that out).

    Container per test really is the easiest, most isolated way to go and requires no special effort.

    Option 2: Modules

    Modules are a nice way to encapsulate sets of registrations and can be a good way to take parameters like this. In this case, I might do something like this for the module:

    public class MyModule : Module
    {
      public bool Testing { get; set; }
    
      protected override void Load(ContainerBuilder builder)
      {
        var toUpdate = new List<IRegistrationBuilder<object, ConcreteReflectionActivatorData, SingleRegistrationStyle>>();
        toUpdate.Add(builder.RegisterType<SomeType>());
        toUpdate.Add(builder.RegisterType<OtherType>());
    
        foreach(var reg in toUpdate)
        {
          if(this.Testing)
          {
            reg.InstancePerTest();
          }
          else
          {
            reg.SingleInstance();
          }
        }
      }
    }
    

    Then you could register it:

    var module = new MyModule { Testing = true };
    builder.RegisterModule(module);
    

    That makes the list of registrations easier to tweak (foreach loop) and also keeps the "things that need changing based on testing" isolated to a module.

    Granted, it could get a little complex in there if you have lambdas and all sorts of other registrations in there, but that's the gist.

    Option 3: Builder Properties

    The ContainerBuilder has a set of properties you can use while building stuff to help avoid having to deal with environment variables but also cart around arbitrary info you can use while setting up the container. You could write an extension method like this:

    public static IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle>
      EnableTesting<TLimit, TActivatorData, TRegistrationStyle>(
        this IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle> registration,
        ContainerBuilder builder)
      {
        if(builder.Properties.TryGetValue("testing", out var testing) && Convert.ToBoolean(testing))
        {
          registration.InstancePerTest();
        }
    
        return registration;
      }
    

    Then when you register things that need to be tweaked, you could do it like this:

    var builder = new ContainerBuilder();
    // Set this in your tests, not in production
    // builder.Properties["testing"] = true;
    builder.RegisterType<Handler>()
      .SingleInstance()
      .EnableTesting(builder);
    var container = builder.Build();
    

    You might be able to clean that up a bit, but again, that's the general idea.

    You might ask why use the builder as the mechanism to transport properties if you have to pass it in anyway.

    • Fluent syntax: Due to the way registrations work, they're all extension methods on the registration, not on the builder. The registration is a self-contained thing that doesn't have a reference to the builder (you can create a registration object entirely without a builder).
    • Internal callbacks: The internals on how registration works basically boil down to having a list of Action executed where the registrations have all the variables set up in a closure. It's not a function where we can pass stuff in during build; it's self-contained. (That might be interesting to change, now I think of it, but that's another discussion!)
    • You can isolate it: You could put this into a module or anywhere else and you won't be adding any new dependencies or logic. The thing carting around the variable will be the builder itself, which is always present.

    Like I said, you could potentially make this better based on your own needs.

    Recommendation: Container Per Test

    I'll wrap up by just again recommending container per test. It's so simple, it requires no extra work, there are no surprises, and it "just works."