-
-
Notifications
You must be signed in to change notification settings - Fork 835
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
Comments
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 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 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 I have seen a couple of workarounds already in quick searches:
The blog article provided has three custom pieces - a custom My gut feeling says the |
Thanks for responding so constructively @tillig - these are good questions. Let me respond as best I can:
Yes, exactly that! It's expressed well by @Pvlerick here:
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 A key part of the doc is the part that details injecting mock services:
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 It's here where I should put my hands up and admit to not being an expert on DI. 😄 The sad death of
|
I may be missing something here, but it should be possible to use a similar workaround without needing to override the /// <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? |
@alistairjevans I tested your alternate workaround in our solution and it works perfectly. Thanks! |
Strike that, let's wait for @johnnyreilly to confirm as the original poster. |
Hey @alistairjevans , Thanks for that - I'll give it a try and report back |
@alistairjevans I tried your solution as well in works perfectly fine for us as well. Thanks a lot! |
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 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 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. |
Cool - I thought maybe you'd seen something that explicitly said something about
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. |
Just to circle back on Back in the ASP.NET Core issue, there was a blog article mentioned illustrating the premise. Create your 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 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. |
Looks good! I had to tweak the code slightly to specify a type parameter of /// <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);
}
} |
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 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 |
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 |
There are lots of ways, like putting an |
I'll certainly blog about it @alistairjevans 👍 Unfortunate to hear that
@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 I had a quick dig on 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. |
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. |
Yup - totally agree. Thanks for your help. |
It really isn't an autofac problem to solve, we just need to spend some time resolving the issue on the hosting side. |
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! |
Another solution without using a custom service provider factory: Configure Autofac in 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);
}
} |
Oliver just to confirm, this also works for .NET 5? |
@matthias-schuchardt Yes, I just stumbled upon this issue while working on a .NET 5 project. |
Just tried it, didn't work. NET 6. |
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. |
Thank you, @LadislavBohm. After roughly 30 hours of trying, this is the solution I was looking for. |
@LadislavBohm That being said 😅 I now get:
when there's more than one Test file (e.g., |
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. |
@LadislavBohm I can confirm that making the classes run sequentially by using the |
.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? |
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. |
Ah, sorry. Yeah, that makes sense now that I think about it. |
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);
}
}
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;
}
|
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>())
... |
Problem Statement
Tragically there's an issue with .NET Core that means that, by default
ConfigureTestContainer
doesn't work. See dotnet/aspnetcore#14907The 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
The text was updated successfully, but these errors were encountered: