如何在集成/端到端测试中启动完整的.NET Core Worker Service?

sam*_*ba2 6 c# integration-testing xunit .net-core

TLDR:为了集成测试 .NET Core Web 应用程序,我们有WebApplicationFactory. 我如何为工人服务做同样的事情?

鉴于此工人服务:

// Program.cs

using SampleWorker;

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services => { services.AddHostedService<Worker>(); })
    .Build();

host.Run();
Run Code Online (Sandbox Code Playgroud)
// Worker.cs

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // - connect to a middleware (data source)
        // - connect to database (data sink)
        // - received events from middleware, transform + persist in database
    }
}
Run Code Online (Sandbox Code Playgroud)

我希望能够编写一个端到端测试,通过测试容器(有效)和被测单元(工作服务)提供数据源和接收器,以某种方式直接通过 .NET Core。对于 Web 应用程序,您将使用WebApplicationFactory<Program>. 我正在寻找一种方法来对 Worker 服务执行相同的操作。

这是我希望能够编写的测试:

public class E2eTest
{
    /// <summary>
    /// Tests the full round trip from an event being
    /// - picked up by the worker service
    /// - processed and transformed
    /// - persisted to the database.
    /// </summary>
    [Fact]
    public void EventIsProcessedAndWrittenToDatabase()
    {
        // arrange
        // - start middleware as data source (testcontainers)
        // - start database as data sink (testcontainers)
        
        // TODO there is no WorkerFactory.
        //      How can I start my SampleWorker with all the config in Program.cs ?
        var unitUnderTest = new WorkerFactory<Program>();
        
        // act
        // - publish an event to the middleware (data source)
        
        // assert
        // - check that there are entries in the database (data sink)
    }
}
Run Code Online (Sandbox Code Playgroud)

通过将 SamplerWorker 也作为 Docker 容器运行,我已经获得了该测试的类似版本。这利用了 Testcontainer 的ImageFromDockerfileBuilder。然而,我希望能够轻松地调试我的被测单元,这对于我当前的 Docker 方法来说变得非常复杂。

有任何想法吗?谢谢

Tas*_*key 1

TL;DR:这是我当前的实现要点。

大致基于微软的WebApplicationFactory,我想出了这个。我确信有些场景它无法处理,并且还有很大的改进空间,但它满足了我的需求:一个进程内运行的辅助服务,就像WebApplicationFactory. 这是在使用HostApplicationBuilder, 而您正在使用IHostBuilder. IHostBuilder但是,如果您想继续使用该代码,它很容易适应。上面链接的要点确实可以IHostBuilder正确处理。

创建一个可发现的方法来挂钩

Microsoft 的测试框架在程序集入口点上查找名为BuildWebHost、CreateWebHostBuilder、 和的方法CreateHostBuilder。框架将调用这些方法来获取创建进程内主机所需的内容。但是,在辅助服务中,我们的入口点是Program.cs,唯一声明的方法是<Main>$。所以我们需要给自己一个方法来检索构建器。我们还将声明Program为public; 这在以后很重要。

我们的Program.cs成为

var builder = CreateHostBuilder(new HostApplicationBuilderSettings { Args = args });
// If there's any builder setup you don't want in the tests, such as auth, do it here.
var host = builder.Build();
host.Run();


public sealed partial class Program
{
    private static HostApplicationBuilder CreateHostBuilder(HostApplicationBuilderSettings settings)
    {
        var builder = Host.CreateApplicationBuilder(settings);
        // The rest of your setup. builder.Services.AddStuff(), etc.
        // Any builder setup you want to make it into the tests
        // MUST be done in here.
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

创建一个工厂来启动主机

现在,我们需要一个工厂,它将采用通用参数,创建我们的构建器,并启动主机。WorkerServiceFactory.cs我为此创建了一个文件。我们还需要第二类来延迟我们的主机启动,以便IAsyncLifetime我们的工厂能够正常工作。我将其命名为“TestWorkerService.cs我们将在此处逐步进行”,下面提供了完整的代码。

注意:您可以执行更高级的工厂方法查找,这可以处理更多场景。我上面链接的要点正是这样做的。我选择在这里保持简单。

TestWorkerService.cs

我们这里所有的工作都可以在构造函数中完成。我们收到一个用于创建主机的构建器参数,以及一个用于配置构建器的参数。然后,我们构建主机并启动它,确保不要阻塞等待主机完成的线程。

public sealed class TestWorkerService : IDisposable
{
    internal readonly IHost Host;

    internal TestWorkerService(
        HostApplicationBuilder builder,
        Action<HostApplicationBuilder> configure
    )
    {
        configure(builder);
        Host = builder.Build();
        Host.StartAsync().GetAwaiter().GetResult();
    }

    public void Dispose()
    {
        Host.Dispose();
    }
}
Run Code Online (Sandbox Code Playgroud)

WorkerServiceFactory.cs

首先,我们需要发现应用程序的入口点。

var entryPoint = typeof(T).Assembly.EntryPoint!.DeclaringType!;
Run Code Online (Sandbox Code Playgroud)

然后,我们将在Program.cs. 我使用一个属性(下面提供)作为方法名称,以便您可以根据需要更改它。您只需要更改 中的方法名称Program.cs,然后覆盖该CreateBuilderMethodName属性。

var hostBuilderFactory = entryPoint.GetMethod(
    CreateBuilderMethodName,
    BindingFlags.Static | BindingFlags.NonPublic
)!;
Run Code Online (Sandbox Code Playgroud)

我们将构造要传递给构建器的设置,将环境名称设置为名为的变量Environment(在下面提供)。然后,我们使用这些设置调用工厂方法并将返回类型转换为HostApplicationBuilder.

var settings = new HostApplicationBuilderSettings
{
    ApplicationName = entryPoint.Assembly.GetName().Name,
    EnvironmentName = Environment
};
_builder = (HostApplicationBuilder)hostBuilderFactory.Invoke(null, new object?[] { settings })!;
Run Code Online (Sandbox Code Playgroud)

现在,我们将创建一个字段和属性来保存我们的TestWorkerService. 如果_workerService我们TestWorkerService使用.​ InitializeAsync()​ IAsyncLifetime例如,这允许在我们调用 时启动并访问 Docker 容器ConfigureHost。

protected virtual void ConfigureHost(HostApplicationBuilder builder) { }

private TestWorkerService? _workerService;
public TestWorkerService WorkerService =>
    _workerService ??= new TestWorkerService(_builder, ConfigureHost);
Run Code Online (Sandbox Code Playgroud)

这是完整的代码:

protected virtual void ConfigureHost(HostApplicationBuilder builder) { }

private TestWorkerService? _workerService;
public TestWorkerService WorkerService =>
    _workerService ??= new TestWorkerService(_builder, ConfigureHost);
Run Code Online (Sandbox Code Playgroud)

创建派生工厂并执行任何最终配置

剩下要做的就是创建您想要在测试中使用的工厂。WorkerServiceFactory为此,我们将从 our 继承,并将 our (现在是公共的)Program作为程序集标记传递。

public abstract class WorkerServiceFactory<T> : IDisposable
{
    private readonly HostApplicationBuilder _builder;

    protected WorkerServiceFactory()
    {
        var entryPoint = typeof(T).Assembly.EntryPoint!.DeclaringType!;
        var hostBuilderFactory = entryPoint.GetMethod(
            CreateBuilderMethodName,
            BindingFlags.Static | BindingFlags.NonPublic
        )!;
        var settings = new HostApplicationBuilderSettings
        {
            ApplicationName = entryPoint.Assembly.GetName().Name,
            EnvironmentName = Environment
        };
        _builder = (HostApplicationBuilder)
            hostBuilderFactory.Invoke(null, new object?[] { settings })!;
    }

    protected virtual void ConfigureHost(HostApplicationBuilder builder) { }

    public void Dispose()
    {
        _workerService?.Dispose();
        ServiceScope.Dispose();
        GC.SuppressFinalize(this);
    }

    public void Start()
    {
        _ = WorkerService;
    }

    protected virtual string CreateBuilderMethodName { get; } = "CreateHostBuilder";
    protected virtual string Environment { get; } = Environments.Development;

    public IServiceProvider Services => WorkerService.Host.Services;

    private TestWorkerService? _workerService;
    public TestWorkerService WorkerService =>
        _workerService ??= new TestWorkerService(_builder, ConfigureHost);

    // Optional: creates a service scope for you.
    /* public TService GetService<TService>()
        where TService : class => ServiceScope.ServiceProvider.GetRequiredService<TService>();

    private IServiceScope? _serviceScope;
    private IServiceScope ServiceScope => _serviceScope ??= Services.CreateScope();*/
}
Run Code Online (Sandbox Code Playgroud)

您现在应该能够像使用 一样使用它WebApplicationFactory,减去一些您可以根据需要添加的更高级功能。