Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support integration tests in .NET Core generic hosting #1207

Closed
johnnyreilly opened this issue Sep 30, 2020 · 33 comments
Closed

Support integration tests in .NET Core generic hosting #1207

johnnyreilly opened this issue Sep 30, 2020 · 33 comments

Comments

@johnnyreilly
Copy link

johnnyreilly commented Sep 30, 2020

Problem Statement

Tragically there's an issue with .NET Core that means that, by default ConfigureTestContainer doesn't work. See dotnet/aspnetcore#14907

The unsealed ContainerBuilder is useful for working around this shortcoming in ASP.NET Core.

Unfortunately it was sealed in #1120

My fear is that without this workaround being available, and without knowing when (or indeed if) it will be fixed in .NET this may end up being a blocker to upgrade from .NET Core 3.1 to .NET 5.

As I see it, those that write integration tests and use AutoFac are going to be unable to upgrade to .NET 5 without first migrating away from AutoFac or forking it. Neither of those is a great outcome. But it's going to be that or deactivate a whole suite of tests (and that's not something that's likely going be possible).

Thanks for all your work on AutoFac BTW - as an open source maintainer myself I appreciate it's a lot of hard work 🌻❤️

Desired Solution

Could I make an appeal for ContainerBuilder to be unsealed please @tillig?

Alternatives You've Considered / Additional Context

My preference would be for this not to be necessary because it's remedied in .NET itself. My fear (and the motivation for raising this) is imagining the outcome of people finding themselves having to choose between using AutoFac 5.0 forever or give up on integration tests.

cc @davidfowl @Tratcher @anurse @javiercn - I realise you're very busy people, but it would be tremendous if this could be considered for fixing in .NET generally so such workarounds wouldn't be necessary. The integration testing that .NET generally supports is tremendous - thanks for your work on it 🤗

@matthias-schuchardt

You can see more details in the linked issue and of the workaround in my blog post here:

https://blog.johnnyreilly.com/2020/05/autofac-webapplicationfactory-and.html

@tillig tillig changed the title Support integration tests in .NET by unsealing ContainerBuilder Support integration tests in .NET Core generic hosting Sep 30, 2020
@tillig
Copy link
Member

tillig commented Sep 30, 2020

I'd like to understand more about the actual goal here, like, from a basic requirements perspective. I'm gathering that the ultimate goal is to be able to write integration tests and provide some overrides for registrations during those tests.

I gather that there used to be a convention by which a method ConfigureTestContainer would automatically be called when a test host was used; however, a change in the .NET 3.x hosting removed that convention.

I think figuring out a way to provide test overrides is interesting. I'd like to focus on that part of things because it allows a bit of flexibility in how we handle it.

I don't think doing something in Autofac to specifically re-enable the now-defunct ConfigureTestContainer convention specifically is as interesting. If it gets fixed in .NET 5, it means we're supporting some "hook" that's only got a limited range of interest - the one thing being addressed in the one framework version. We already have some crazy stuff we have to support due to changes between .NET Core prior to 2.1 and 2.1 proper. I don't think unsealing a class is that level of challenge to maintain, but ContainerBuilder was never meant to be extended in that fashion and we have seen some questions come in about why XYZ doesn't work when deriving from ContainerBuilder. It's a non-zero support burden for an extension point that wasn't intended - the major release was the opportunity to stop having to support that.

So, given all that, I'd be interested in coming up with some various alternatives to solving the more general issue of how to provide test registration overrides in a consistent and reliable way. It does mean that some integration tests may need to be refactored or changed if the workaround isn't literally "enable ConfigureTestContainer to be supported." From an Autofac perspective, I'm OK with that. Autofac v6 has a number of breaking changes and this is potentially one of the effects of that. If we can still determine a good way to solve that more general issue, I'd be OK.

I have seen a couple of workarounds already in quick searches:

  • Create a TestStartup class that derives from Startup: In this scenario, the standard application's Startup class and has ConfigureContainer marked as virtual. In a class TestStartup that derives from Startup, the ConfigureContainer method is overridden, base is called as needed, and test overrides are registered in that TestStartup class.
  • Create a custom AutofacServiceProviderFactory that allows for an additional configuration action: In this scenario, the custom factory takes two optional parameters for configuration actions - one that runs every time a service provider is created (what's already there) and one that is specifically "additional test actions" that would only be provided during testing.

The blog article provided has three custom pieces - a custom ContainerBuilder, AutofacServiceProviderFactory, and WebApplicationFactory. I am not yet entirely clear on what exactly is happening in the custom ContainerBuilder but it seems the general idea is to locate all the IStartupConfigureContainerFilter<T> instances and execute those in order in an effort to also execute ConfigureTestContainer directly in the correct order. It feels like that resolution and execution could be elsewhere, that maybe it doesn't need to be literally in a custom ContainerBuilder but, as noted, I've not really dived in deep here.

My gut feeling says the TestStartup derive-and-override mechanism seems like a reasonable way to address the challenge since it still allows for test overrides to be provided in a pretty clean and isolated fashion. However, it would still be interesting to see what it might take to support this ConfigureTestContainer + filter thing with Autofac v6, even if just as a proof of concept to understand the limitations and challenges. I'm also not sure if there's a reason that TestStartup mechanism wouldn't work for some reason - I'd be interested in hearing why these other things are unacceptable, other than "it's not the same as it was before."

@johnnyreilly
Copy link
Author

johnnyreilly commented Oct 1, 2020

Thanks for responding so constructively @tillig - these are good questions. Let me respond as best I can:

I'd like to understand more about the actual goal here, like, from a basic requirements perspective. I'm gathering that the ultimate goal is to be able to write integration tests and provide some overrides for registrations during those tests.

Yes, exactly that! It's expressed well by @Pvlerick here:

We have a lot of narrow integration tests that are redefining some of the dependencies and swapping them with mocks - calls to services.

Essentially there's a class of integration test that spins up a test version of an application, which you can fire requests at. It's a "real" application by which I mean it runs using the StartUp.cs class and uses the code in there including the dependency injection (in my case, AutoFac). It's built upon the WebApplicationFactory class which streamlines bootstrapping the SUT with TestServer. There's some great documentation written by @ardalis @javiercn and @jvandertil:

https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-3.1#customize-webapplicationfactory

A key part of the doc is the part that details injecting mock services:

Services can be overridden in a test with a call to ConfigureTestServices on the host builder. To inject mock services, the SUT must have a Startup class with a Startup.ConfigureServices method.

This injection of mock services is the secret sauce that we're after. You definitely understand this but I wanted to be clear.

The code written to enable ConfigureTestContainer in the linked blog isn't really mine, it's based on @ssunkari's code here and @jr01's here. I've read it and re-read it but I don't full understand the implications of all of it.

It's here where I should put my hands up and admit to not being an expert on DI. 😄

The sad death of ConfigureTestContainer?

I was struck by this sentence here:

I don't think doing something in Autofac to specifically re-enable the now-defunct ConfigureTestContainer convention specifically is as interesting.

Is it official that ConfigureTestContainer is deprecated then? That would explain why there's no reference to it in the Microsoft docs on integration testing. Do you know why ConfigureTestServices (which is the bedfellow of ConfigureServices) seems to be living on whereas ConfigureTestContainer (compadre of ConfigureContainer) is not? That's new (and puzzling!) news to me.

Maybe TestStartup?

The approach you outline here looks promising:

Create a TestStartup class that derives from Startup: In this scenario, the standard application's Startup class and has ConfigureContainer marked as virtual. In a class TestStartup that derives from Startup, the ConfigureContainer method is overridden, base is called as needed, and test overrides are registered in that TestStartup class.

I'd like to understand the idea a bit better. I've an observation and a question.

The observation is that tests will become more verbose with this approach. If I understand it correctly, you'll probably create a TestStartup for each class of tests. So lot's of TestStartups and more code. Not the end of the world if it works though! Something that works is the goal.

The question exposes my ignorance around AutoFac - so please bear with me.

So imagine I've got my TestStartup class which overrides ConfigureContainer. The first thing it does call base.ConfigureContainer which invokes a class that looks something like this:

public virtual void ConfigureContainer(ContainerBuilder builder) {
    builder.RegisterAssemblyModules(typeof(My.Common.Infrastructure.Registry).Assembly);
    builder.RegisterAssemblyModules(typeof(My.Domain.Infrastructure.Registry).Assembly);
    builder.RegisterAssemblyModules(typeof(My.Data.Infrastructure.Registry).Assembly);
    builder.RegisterAssemblyModules(typeof(My.Database.Infrastructure.Registry).Assembly);
    // ...
}

As you can see, this mechanism registers dependencies across a whole bunch of assemblies. I now want to replace say, my ISomethingService with a mock in my TestStartup. The ISomethingService has been registered in amongst the calls above. Could I successfully do something like the following?

public override void ConfigureContainer(ContainerBuilder builder) {
    base.ConfigureContainer(builder);

    var fakeISomethingService = A.Fake<ISomethingService>(); // using FakeItEasy
    builder.RegisterInstance(fakeISomethingService );
    // ...
}

This would override the previous ISomethingService registration that took place in the base call I think? You think this should work as an approach?

@alistairjevans
Copy link
Member

I may be missing something here, but it should be possible to use a similar workaround without needing to override the ContainerBuilder. I just tried this:

/// <summary>
/// Based upon https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/test/integration-tests/samples/3.x/IntegrationTestsSample
/// </summary>
/// <typeparam name="TStartup"></typeparam>
public class AutofacWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
    protected override IHost CreateHost(IHostBuilder builder)
    {
        builder.UseServiceProviderFactory(new CustomServiceProviderFactory());
        return base.CreateHost(builder);
    }
}

/// <summary>
/// Based upon https://github.com/dotnet/aspnetcore/issues/14907#issuecomment-620750841 - only necessary because of an issue in ASP.NET Core
/// </summary>
public class CustomServiceProviderFactory : IServiceProviderFactory<ContainerBuilder>
{
    private AutofacServiceProviderFactory _wrapped;
    private IServiceCollection _services;

    public CustomServiceProviderFactory()
    {
        _wrapped = new AutofacServiceProviderFactory();
    }

    public ContainerBuilder CreateBuilder(IServiceCollection services)
    {
        // Store the services for later.
        _services = services;

        return _wrapped.CreateBuilder(services);
    }

    public IServiceProvider CreateServiceProvider(ContainerBuilder containerBuilder)
    {
        var sp = _services.BuildServiceProvider();
#pragma warning disable CS0612 // Type or member is obsolete
        var filters = sp.GetRequiredService<IEnumerable<IStartupConfigureContainerFilter<ContainerBuilder>>>();
#pragma warning restore CS0612 // Type or member is obsolete

        foreach (var filter in filters)
        {
            filter.ConfigureContainer(b => { })(containerBuilder);
        }

        return _wrapped.CreateServiceProvider(containerBuilder);
    }        
}

This then works:

public async Task MyAsync()
{
    void ConfigureTestServices(IServiceCollection services)
    {
        services.AddSingleton("");
    }

    void ConfigureTestContainer(ContainerBuilder builder)
    {
        builder.RegisterInstance("hello world");
    }

    var factory = new AutofacWebApplicationFactory<DefaultStartup>();

    using var client = factory
        .WithWebHostBuilder(builder => {
            builder.ConfigureTestServices(ConfigureTestServices);
            builder.ConfigureTestContainer<ContainerBuilder>(ConfigureTestContainer);
        })
        .CreateClient();
}

@johnnyreilly, perhaps you could try out my alternate workaround and see if I've forgotten something?

@jr01
Copy link

jr01 commented Oct 1, 2020

@alistairjevans I tested your alternate workaround in our solution and it works perfectly. Thanks!

@alistairjevans
Copy link
Member

alistairjevans commented Oct 1, 2020

Good stuff; would you mind posting into the .NET Core issue that's been referenced, so people know there's a solution for Autofac?

Strike that, let's wait for @johnnyreilly to confirm as the original poster.

@johnnyreilly
Copy link
Author

Hey @alistairjevans ,

Thanks for that - I'll give it a try and report back

@matthias-schuchardt
Copy link

@alistairjevans I tried your solution as well in works perfectly fine for us as well. Thanks a lot!

@tillig
Copy link
Member

tillig commented Oct 1, 2020

the now-defunct ConfigureTestContainer convention

Is it official that ConfigureTestContainer is deprecated then?

I don't know. Autofac isn't a Microsoft product and none of the maintainers, including me, work for Microsoft. However, the whole issue posted over there is about how ConfigureTestContainer isn't functioning, which is the definition of defunct. I specifically said "defunct" and not "deprecated." You'll have to ask the Microsoft folks if it's deprecated.

The larger issue is that it's not Autofac responsibility to fix it or specifically support workarounds. It seems there's a reasonable solution that doesn't require unsealing ContainerBuilder so I think we have an answer.

As soon as we get confirmation this works from @johnnyreilly we can close this. Unclear if it's something we need to document or provide an example for.

@johnnyreilly
Copy link
Author

I don't know. Autofac isn't a Microsoft product and none of the maintainers, including me, work for Microsoft. However, the whole issue posted over there is about how ConfigureTestContainer isn't functioning, which is the definition of defunct. I specifically said "defunct" and not "deprecated." You'll have to ask the Microsoft folks if it's deprecated.

Cool - I thought maybe you'd seen something that explicitly said something about ConfigureTestContainers future (or lack thereof). No worries if not.

As soon as we get confirmation this works from @johnnyreilly we can close this. Unclear if it's something we need to document or provide an example for.

Just testing now. There might be a tweak needed - will report back. If this does work out then I'll likely blog about this (as blogging is essentially my drop-in replacement for long term memory). Would be happy to submit a docs PR given some guidance around what could be useful.

@tillig
Copy link
Member

tillig commented Oct 1, 2020

Just to circle back on TestStartup (since I was on my phone earlier, and coding on a phone is sub-par at best)... it's not actually that much extra code - not really any more or different than maintaining a ConfigureTestContainer method, at least.

Back in the ASP.NET Core issue, there was a blog article mentioned illustrating the premise.

Create your Startup class as normal, but make ConfigureContainer virtual.

public class Startup
{
  // Extra stuff omitted for clarity.
  public virtual void ConfigureContainer(ContainerBuilder builder)
  {
    // The usual real application registrations.
  }
}

Then for your tests, you have a derived class.

public class TestStartup : Startup
{
  public override void ConfigureContainer(ContainerBuilder builder)
  {
    base.ConfigureContainer(builder);
    // Now register your test overrides.
  }
}

Now in places where you need your test stuff registered, like when specifying the TStartup in WebApplicationFactory<TStartup>, use the TestStartup class instead of the one from your app. Basically instead of keeping the test registrations in a method called ConfigureTestContainer, it's in an override in a different class. Still separate, still roughly the same amount of code.

The reason I'm not sure if this is interesting to document or not is because it's such a niche case that, again, isn't really a problem with Autofac. It's not "here's how you use Autofac," it's "here's how to work around an issue that is being dealt with elsewhere."

If you really wanted to solve it for folks, I'd recommend figuring out what the reusable parts of the solution are and publishing a small package on NuGet that people can consume as the workaround - codify the solution and support it for the community. Autofac's already neck deep in integration libraries for project types on which we're not really experts so this isn't something we'll be providing. We encourage the community to publish their own integration/support packages directly.

@johnnyreilly
Copy link
Author

Looks good!

I had to tweak the code slightly to specify a type parameter of UseServiceProviderFactory<ContainerBuilder>. Otherwise looks good - have migrated my application to use this:

    /// <summary>
    /// Based upon https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/test/integration-tests/samples/3.x/IntegrationTestsSample
    /// </summary>
    /// <typeparam name="TStartup"></typeparam>
    public class AutofacWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
    {
        protected override IHost CreateHost(IHostBuilder builder)
        {
            builder.UseServiceProviderFactory<ContainerBuilder>(new CustomServiceProviderFactory());
            return base.CreateHost(builder);
        }
    }

    /// <summary>
    /// Based upon https://github.com/dotnet/aspnetcore/issues/14907#issuecomment-620750841 - only necessary because of an issue in ASP.NET Core
    /// </summary>
    public class CustomServiceProviderFactory : IServiceProviderFactory<ContainerBuilder>
    {
        private AutofacServiceProviderFactory _wrapped;
        private IServiceCollection _services;

        public CustomServiceProviderFactory()
        {
            _wrapped = new AutofacServiceProviderFactory();
        }

        public ContainerBuilder CreateBuilder(IServiceCollection services)
        {
            // Store the services for later.
            _services = services;

            return _wrapped.CreateBuilder(services);
        }

        public IServiceProvider CreateServiceProvider(ContainerBuilder containerBuilder)
        {
            var sp = _services.BuildServiceProvider();
#pragma warning disable CS0612 // Type or member is obsolete
            var filters = sp.GetRequiredService<IEnumerable<IStartupConfigureContainerFilter<ContainerBuilder>>>();
#pragma warning restore CS0612 // Type or member is obsolete

            foreach (var filter in filters)
            {
                filter.ConfigureContainer(b => { })(containerBuilder);
            }

            return _wrapped.CreateServiceProvider(containerBuilder);
        }        
    }

@johnnyreilly
Copy link
Author

johnnyreilly commented Oct 1, 2020

Now in places where you need your test stuff registered, like when specifying the TStartup in WebApplicationFactory<TStartup>, use the TestStartup class instead of the one from your app. Basically instead of keeping the test registrations in a method called ConfigureTestContainer, it's in an override in a different class. Still separate, still roughly the same amount of code.

I was pondering this and I'm not so clear how it would work in terms of supplying dependencies. As I understand it, when working with WebApplicationFactory<TestStartup> you don't have access to the TestStartup that ends up being instantiated. That being the case, how are you able to provide mocks / fakes which you can assert on later in a test?

Consider a test like this:

        [Fact]
        public async Task its_a_test() {
            // Arrange
            var fakeService = A.Fake<IThingService>();

            A.CallTo(() => fakeService.GetThing(A<string>.Ignored))
                .Returns(Task.FromResult(42));

            void ConfigureTestContainer(ContainerBuilder builder) {
                builder.RegisterInstance(fakeService);
            }

            var client = _factory
        .WithWebHostBuilder(builder => {
            builder.ConfigureTestServices(ConfigureTestServices);
            builder.ConfigureTestContainer<ContainerBuilder>(ConfigureTestContainer);
        })
                .CreateClient();

            // Act
            var response = await client.GetAsync("api/thingummy");

            // Assert
            response.EnsureSuccessStatusCode();
            var responseContent = await response.Content.ReadAsStringAsync();
            responseContent.Should().Be("Blarg");

            A.CallTo(() => fakeService.GetThing(A<string>.Ignored)).MustHaveHappened();
        }

With the TestStartup approach it's not clear to me how you could supply fakeService to the TestStartup so that you could subsequently assert on it.

@alistairjevans
Copy link
Member

Thanks for checking that @johnnyreilly; my feeling is that all of this is just a workaround for an issue that is already tracked in another repo, as @tillig states. I'm not sure it's something Autofac needs to document; it's something to blog about, post a link to said post in the .NET repo issue, and that's probably it.

Creating a community package with the workaround feels like it could have downsides by cementing this approach as in any way recommended. The IStartupConfigureContainerFilter that the workaround depends on is already marked as deprecated by the .NET team, so it won't necessarily work forever anyway.

@tillig
Copy link
Member

tillig commented Oct 1, 2020

There are lots of ways, like putting an AsyncLocal reference in there to provide a module to register. My point isn't to give you step by step instructions, it's to open up avenues for additional research and experimentation. I'd wager some folks have a fixed set of fakes to insert for a set of integration tests and this would be perfect for that.

@johnnyreilly
Copy link
Author

I'll certainly blog about it @alistairjevans 👍

Unfortunate to hear that IStartupConfigureContainerFilter is deprecated.

There are lots of ways, like putting an AsyncLocal reference in there to provide a module to register. My point isn't to give you step by step instructions

@tillig apologies if it comes across as me being difficult. I'm afraid I don't quite follow what you're suggesting. I'm sure you're correct that there are many ways to tackle accessing an instance of TestStartup when you're not responsible for newing up the TestStartup up.

I had a quick dig on AsyncLocal but I didn't quite follow what you meant. Sorry.

If anyone else would like to pitch in with suggestions that'd be gratefully received. But no worries if not.

My feeling is that this is a problem that will return and so this is an opportunity to document an approach which could serve the wider community.

I'm very mindful that this is working around a problem in .NET - it's not a problem created by Autofac in any way. Unfortunately it does affect the users of Autofac which is suboptimal but such is life.

@tillig
Copy link
Member

tillig commented Oct 1, 2020

It's not just an issue for Autofac users, which is the point I'm trying to make. But it doesn't really matter; it seems like there's a workable solution and there's nothing further needed at the Autofac level so I'm closing this issue.

@tillig tillig closed this as completed Oct 1, 2020
@johnnyreilly
Copy link
Author

Yup - totally agree. Thanks for your help.

@davidfowl
Copy link

It really isn't an autofac problem to solve, we just need to spend some time resolving the issue on the hosting side.

@johnnyreilly
Copy link
Author

I've blogged about the workaround here: https://blog.johnnyreilly.com/2020/10/autofac-6-integration-tests-and-generic-hosting.html - full credit to @alistairjevans for providing the approach. Thanks chap!

@oliverhanappi
Copy link

oliverhanappi commented May 28, 2021

Another solution without using a custom service provider factory:

Configure Autofac in Program instead of Startup.ConfigureContainer(ContainerBuilder):

public static IHostBuilder CreateHostBuilder (string[] args)
{
  return Host.CreateDefaultBuilder (args)
    .UseServiceProviderFactory (new AutofacServiceProviderFactory())
    .ConfigureContainer<ContainerBuilder>(b => { /* configure Autofac here */ })
    .ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>());
}

Override Autofac registrations using ContainerBuilder in a derived WebApplicationFactory:

public class TestWebApplicationFactory : WebApplicationFactory<Startup>
{
  protected override IHost CreateHost(IHostBuilder builder)
  {
    builder.ConfigureContainer<ContainerBuilder>(b => { /* test overrides here */ });
    return base.CreateHost(builder);
  }
}

@matthias-schuchardt
Copy link

Another solution without using a custom service provider factory:

Configure Autofac in Program instead of Startup.ConfigureContainer(ContainerBuilder):

Oliver just to confirm, this also works for .NET 5?

@oliverhanappi
Copy link

@matthias-schuchardt Yes, I just stumbled upon this issue while working on a .NET 5 project.

@ksydex
Copy link

ksydex commented Nov 23, 2021

Another solution without using a custom service provider factory:

Configure Autofac in Program instead of Startup.ConfigureContainer(ContainerBuilder):

public static IHostBuilder CreateHostBuilder (string[] args)
{
  return Host.CreateDefaultBuilder (args)
    .UseServiceProviderFactory (new AutofacServiceProviderFactory())
    .ConfigureContainer<ContainerBuilder>(b => { /* configure Autofac here */ })
    .ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>());
}

Override Autofac registrations using ContainerBuilder in a derived WebApplicationFactory:

public class TestWebApplicationFactory : WebApplicationFactory<Startup>
{
  protected override IHost CreateHost(IHostBuilder builder)
  {
    builder.ConfigureContainer<ContainerBuilder>(b => { /* test overrides here */ });
    return base.CreateHost(builder);
  }
}

Just tried it, didn't work. NET 6.
New service is registering though, but it's on top of the list, that is returned from scopedServices.GetServices<T>();, so, the original one service is being resolved everywhere. Thoughts?

@LadislavBohm
Copy link

It doesn't work for me either when using AutoFac Modules because they get called last even after ConfigureTestServices.

Workaround I am using is following.

public class Startup {
    //holds all overrides that might be defined (in tests for example)
    private readonly Queue<Action<ContainerBuilder>> m_AdditionalServiceRegistrations = new();

    public void ConfigureServices(IServiceCollection services) {
        //standard framework services....

       //register Startup class itself to be available in tests
       if (Environment.IsTest()) {
        services.AddSingleton(GetType(), this);
      }
    }

    public void ConfigureContainer(ContainerBuilder builder) {
      //standard module registrations
      builder.RegisterModule<ModuleA>();

      //as last dequeue all available overrides in order and apply them
      while (m_AdditionalServiceRegistrations.TryDequeue(out var registrations)) {
        registrations(builder);
      }
    }
   
    //method called in tests to register possible mock services
    public void AddServiceRegistrationOverrides(Action<ContainerBuilder> registration) {
      m_AdditionalServiceRegistrations.Enqueue(registration);
    }
}

Then define extension method similar to this one

  internal static class HostBuilderExtensions {
    internal static IWebHostBuilder UseAutofacTestServices(this IWebHostBuilder builder, Action<ContainerBuilder> containerBuilder) {
      builder.ConfigureTestServices(services => {
        //startup is registered only in test environment so we can be sure it's available here
        var startup = (Startup)services.Single(s => s.ServiceType == typeof(Startup)).ImplementationInstance!;

        startup.AddServiceRegistrationOverrides(containerBuilder);
      });

      return builder;
    }
}

And finally I can use it in XUnit test with ITestFixture

public class SomeTestClass: IClassFixture<WebApplicationFactory> {
    private readonly WebApplicationFactory m_Factory;

    public SomeTestClass(WebApplicationFactory factory) {
      m_Factory = factory;
    }

    [Fact]
    public async Task SomeTestMethod() {
        await using var updatedFactory = m_Factory.WithWebHostBuilder(builder => {
        builder.UseAutofacTestServices(containerBuilder => {
          containerBuilder.RegisterType<MockService>().As<IService>().SingleInstance();
        });
      });
    }
}

Disadvantage is that you "pollute" your Startup class a bit but I think if it's a concern to you you can do these modifications in some derived TestStartup class for example.

@jsdevtom
Copy link

Thank you, @LadislavBohm. After roughly 30 hours of trying, this is the solution I was looking for.

@jsdevtom
Copy link

It doesn't work for me either when using AutoFac Modules because they get called last even after ConfigureTestServices.

Workaround I am using is following.

@LadislavBohm That being said 😅 I now get:

Autofac.Core.DependencyResolutionException An exception was thrown while activating <projectname>.server.StartupHook -> λ:FluentMigrator.Runner.IVersionLoader.

when there's more than one Test file (e.g., Registration.Integration.Tests.cs and Login.Integration.Tests.cs). If I move the tests from one file to the other so that there is only one test file with actual tests inside, the problem doesn't occur. Do you have any idea how to solve this?

@LadislavBohm
Copy link

It doesn't work for me either when using AutoFac Modules because they get called last even after ConfigureTestServices.
Workaround I am using is following.

@LadislavBohm That being said 😅 I now get:

Autofac.Core.DependencyResolutionException An exception was thrown while activating <projectname>.server.StartupHook -> λ:FluentMigrator.Runner.IVersionLoader.

when there's more than one Test file (e.g., Registration.Integration.Tests.cs and Login.Integration.Tests.cs). If I move the tests from one file to the other so that there is only one test file with actual tests inside, the problem doesn't occur. Do you have any idea how to solve this?

I think it might be because multiple files can be executed in parallel (at least by XUnit) so what you can try is to tell XUnit to never execute tests in parallel and see if it helps. If it does and you want to execute them in parallel then you will need to make this solution thread-safe which shouldn't be too difficult I think.

@jsdevtom
Copy link

@LadislavBohm I can confirm that making the classes run sequentially by using the Collection Attribute solves the problem. How could I make the solution thread safe?

@derekgreer
Copy link

.Net 6 makes this even harder, as you are pushed away from a Startup that you'd be able to at least add virtual methods to. Anyone working on this?

@tillig
Copy link
Member

tillig commented May 21, 2022

If anyone's working on it, it's part of .NET Core, not something Autofac will be providing. You'd need to follow up over there.

@derekgreer
Copy link

Ah, sorry. Yeah, that makes sense now that I think about it.

@pavliy
Copy link

pavliy commented Jul 19, 2022

Had the same issue. Resolved using custom WebApplicationFactory, which looks like this:

public class CustomWebApplicationFactory<T> : WebApplicationFactory<T>
    where T : class
{
    protected override IHost CreateHost(IHostBuilder builder)
    {
        builder
            .UseServiceProviderFactory(new IntegrationTestServiceProviderFactory())
            .ConfigureAutofacContainer()
            .ConfigureContainer<ContainerBuilder>(
                (_, containerBuilder) =>
                    {
                        containerBuilder.RegisterAssemblyModules(typeof(IntegrationTestAssembly).Assembly);
                    })
            .ConfigureServices(
                services =>
                    {
                        services
                            .AddControllers()
                            .AddApplicationPart(typeof(Program).Assembly);
                    });
        return base.CreateHost(builder);
    }
}
  1. ConfigureAutofacContainer - is an extension method which is called in original Program.cs and inside this custom integration test setup as well
public static IHostBuilder ConfigureAutofacContainer(this IHostBuilder hostBuilder)
    {
        hostBuilder.ConfigureContainer<ContainerBuilder>(
            (_, builder) =>
                {
                    builder.RegisterLogger();
                    builder.RegisterAssemblyModules(typeof(ApplicationServicesAssembly).Assembly);
                    builder.RegisterAssemblyModules(typeof(InfrastructureAssembly).Assembly);
                });
        return hostBuilder;
    }
  1. Right after it - loading Autofac module from IntegrationTestAssembly - there all my needed overrides are located

@tuskajozsef
Copy link

tuskajozsef commented Oct 31, 2023

Cleanest solution I could achieve based on previous answers:

Create overrides:

public class TestAutofacModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<TestService>().As<IService>();
    }
}

Create new ServiceProviderFactory:

public class TestServiceProviderFactory<T> : IServiceProviderFactory<ContainerBuilder> where T: Module, new()
{
    private readonly AutofacServiceProviderFactory _factory = new();

    public ContainerBuilder CreateBuilder(IServiceCollection services)
    {
        return _factory.CreateBuilder(services);
    }

    public IServiceProvider CreateServiceProvider(ContainerBuilder containerBuilder)
    {
        containerBuilder.RegisterModule(new T());
        return _factory.CreateServiceProvider(containerBuilder);
    }
}

Use new Factory:

var builder = new HostBuilder()
                .UseServiceProviderFactory(new TestServiceProviderFactory<TestAutofacModule>())
                ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests