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

IAsyncLifetime support? #67

Open
MichaelLogutov opened this issue Jun 8, 2022 · 17 comments
Open

IAsyncLifetime support? #67

MichaelLogutov opened this issue Jun 8, 2022 · 17 comments

Comments

@MichaelLogutov
Copy link

MichaelLogutov commented Jun 8, 2022

Hello.
Is IAsyncLifetime supported? Because it seems InitializeAsync is not called for dependencies. Or is there an alternative way to invoke async initialization for fixtures (like databases)?

@pengweiqhca
Copy link
Owner

Can you provide sample code?

@MichaelLogutov
Copy link
Author

Sure. This test not passing.

using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace Tests;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<MyFixture>();
    }
}

public class MyFixture : IAsyncLifetime
{
    public string Data { get; set; } = "Initialize not called";


    public Task InitializeAsync()
    {
        this.Data = "Initialize called";
        return Task.CompletedTask;
    }


    public Task DisposeAsync() => Task.CompletedTask;
}

public class Tests
{
    private readonly MyFixture fixture;


    public Tests(MyFixture fixture)
    {
        this.fixture = fixture;
    }


    [Fact]
    public void Test()
    {
        Assert.Equal("Initialize called", this.fixture.Data);
    }
}

@pengweiqhca
Copy link
Owner

pengweiqhca commented Jun 9, 2022

Oh, i see. IAsyncLifetime is xunit native support, maybe you can see this test case or consider use IHostedService.

@MichaelLogutov
Copy link
Author

The native xunit fixture injection does not work for injection in other fixtures. That was the main reason I used
Xunit.DependencyInjection - to be able to inject fixtures in other fixtures. And it works great, but without async initialization. So I thought maybe there is some other way to call InitializeAsync right after instance creation?

@pengweiqhca
Copy link
Owner

DI cannot call InitialAsync, you should put all the code that needs to be initialized in a IHostedService (Creating test class instance and class fixture instance are different service scopes).

@MichaelLogutov
Copy link
Author

IHostedService used to run some background jobs alongside with app. In my case I just want some kind of async constructor and I'm not really sure how you suggest to use IHostedService to call async inits right after instances of fixtures has been created but before they've been used in tests. I thought that DI could use some kind of interceptor to invoke InitializeAsync for any instanced object of type IAsyncLifetime.

@pengweiqhca
Copy link
Owner

Use nest startup?

@MichaelLogutov
Copy link
Author

I think it won't help. I'll try to explain what I need.
I'm writing integration tests - using docker compose I launch different dependencies (like databases, rabbitmq, etc). To work with those dependencies from tests I write fixtures - a services that encapsulate communication logic with the given dependency.

But there are restrictions:

  1. Before using any method from those fixtures the underlying dependency needs to be initialized - for example, database needs to be cleaned or new databases created. So initialization needs to be performed before fixture can be used in tests - so it's right after fixture instance creation.
  2. Fixtures can be used inside other fixutres - for example, http-mock server fixture could be used in several other fixtures. So there should be worked DI not only in tests, but in fixtures as well.
  3. Those initialization operations are almost always asynchronous. So it's best to be able to call them async native. Currently I just call with GetAwaiter().GetResult() in constructor, but it's less than ideal. IAsyncLifetime solved that problem, but it does not support DI in other fixtures.

@WeihanLi
Copy link
Contributor

WeihanLi commented Sep 20, 2023

@MichaelLogutov think maybe the IHostedService could help with this, I used to use the IHostedService to
waiting for the service ready, and you can also use another IHostedService in it via the dependency injection

And if you want to ensure your services start before running IHostedService maybe the new IHostedLifecycleService(coming in .NET8) would help, we could use StartingAsync to wait before Start host services

@MichaelLogutov
Copy link
Author

@WeihanLi If I understood correctly, this will ends up with me calling async code with GetAwaiter().GetResult() on dependency which is not ideal solution.

@WeihanLi
Copy link
Contributor

WeihanLi commented Sep 20, 2023

@MichaelLogutov think it depends, we could try to avoid using async-based code in the constructor, move the async startup logic code into the StartAsync instead
I updated the sample for the test server integration

I created a RedisHealthCheckService and DbHealthCheckService, and the DbHealthCheckService has a dependency on RedisHealthCheckService, hope it would help

@MichaelLogutov
Copy link
Author

Well, it could work yes. It's still not quite versatile as pytest fixtures because I need to register all those fixtures as global hosted services, which means they always created compared to pytest fixtures that will be initialized as needed. But it's still possible solution. Thanks.

@MichaelLogutov
Copy link
Author

Sorry for the long delay, just got time to finally test your sample. Correct me if I'm wrong, but with you solution it will be required to manually write "wait for dependencies to be ready" loops?

@WeihanLi
Copy link
Contributor

WeihanLi commented Dec 5, 2023

@MichaelLogutov sorry for the late reply

yes, if you want to control the dependencies startup sequence exactly, I think we still need to wait for ready in a specific service.

and there's IHostedLifecycleService(since .NET 8) which introduced StartingAsync would invoked before the StartAsync method, which could be used to run some pre-initialize tasks, maybe we could move some
hard dependencies to the StartingAsync so that we could avoid waiting for these dependencies ready, samples updated

edit:

sample updated

@MichaelLogutov
Copy link
Author

Pity, but for now, I've ended up using service locator instead of constructor injection in test cases with this code:

using System;
using System.Threading.Tasks;
using SimpleInjector;
using SimpleInjector.Lifestyles;
using Xunit;

namespace Tests;

public abstract class IntegrationTestFixture
{
    public bool Initialized { get; private set; }


    public virtual Task InitializeAsync()
    {
        this.Initialized = true;
        return Task.CompletedTask;
    }
}

public class MyFixtureA : IntegrationTestFixture
{
}

public class MyFixtureB : IntegrationTestFixture
{
    private readonly MyFixtureA fixtureA;


    public MyFixtureB(MyFixtureA fixtureA)
    {
        this.fixtureA = fixtureA;
    }


    public override async Task InitializeAsync()
    {
        if (!this.fixtureA.Initialized)
            throw new InvalidOperationException("FixtureA is not initialized");

        await base.InitializeAsync();
    }
}

public static class TestPlatform
{
    public static readonly Container Container;


    static TestPlatform()
    {
        Container = new Container
        {
            Options =
            {
                AllowOverridingRegistrations = true,
                DefaultScopedLifestyle = new AsyncScopedLifestyle(),
                ResolveUnregisteredConcreteTypes = true,
                EnableAutoVerification = false
            }
        };

        Container.RegisterInitializer<IntegrationTestFixture>(x =>
        {
            x.InitializeAsync().GetAwaiter().GetResult();
        });

        foreach (var type in Container.GetTypesToRegister<IntegrationTestFixture>())
            Container.RegisterSingleton(type);

        Container.Verify();
    }
}

public class Tests
{
    private readonly MyFixtureB fixtureB = TestPlatform.Container.GetInstance<MyFixtureB>();


    [Fact]
    public void Test()
    {
        Assert.True(this.fixtureB.Initialized);
    }
}

This enabled to just write fixture with injection of any other fixture and upon creating instance DI container (SimpleInjector in this case) will ensure that InitializeAsync is called in the right order automatically.

@goldsam
Copy link

goldsam commented Jan 3, 2024

@pengweiqhca I have discovered that the IHostedService has a different lifecycle from IAsyncLifetime that can sometimes cause tests in CI to hang during test discovery. In my case, I was using Testcontainers and Testcontainers.CosmosDb and managing the container lifecycle in an IHostedService implementation. I believe the issue was that the host lifecycle was being invoked during test discovery which had unexpected consequences for that library.

It took me days to debug this issue and the solution was simply to use IAsyncLifetime instead of IHostedService. I experienced this bug with both .net8.0 and .net7.0 test host. Proper IAsyncLifetime seems to be a critical missing feature.

Also, I have not been able to inject a test fixture as a dependency of my test's dependency (effectively a nested fixture). More specifically, I wanted to inject the fixture when configuring IOptions as follows in my Startup class:

public partial class Startup
{
    public void ConfigureHost(IHostBuilder hostBuilder)
    {
        hostBuilder
            .ConfigureServices(services =>
            {
                services.AddOptions<DependencyOptions>()
                    .Configure<TestFixture>((options, fixture) =>
                    {
                        options.Value = fixture.Value;
                    });
                // ...
            });
    }
}

Unfortunately, this doesn't work. Perhaps I should create a separate issue for this?

@MichaelLogutov
Copy link
Author

I've ended up registering testcontainers fixtures as singletons and just blocking on async in constructor.

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

4 participants