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

问题描述 投票:0回答:1

TLDR:要集成测试 .NET Core Web 应用程序,我们有

WebApplicationFactory
我如何为工人服务做同样的事情?

鉴于此工人服务:

// Program.cs

using SampleWorker;

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

host.Run();
// 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
    }
}

我希望能够编写一个端到端测试,通过测试容器(有效)和被测单元(worker 服务)以某种方式直接通过 .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)
    }
}

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

有什么想法吗?谢谢🙂

c# .net-core integration-testing xunit
1个回答
0
投票

大致基于微软的

WebApplicationFactory
,我想出了这个。我确信有些场景它无法处理,并且还有很大的改进空间,但它得到了我想要的:一个在进程中运行的工作服务,就像
WebApplicationFactory
一样。

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

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, 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.
        ...
    }
}

创建一个工厂来启动主机

现在,我们需要一个工厂,它将采用通用参数,创建我们的构建器,并启动主机。我为此创建了一个名为

WorkerServiceFactory.cs
的文件。我们所有的工作都可以在类构造函数中完成。我们将在这里一步一步地进行,下面提供了完整的课程。 首先,我们需要发现应用程序的入口点。

var entryPoint = typeof(T).Assembly.EntryPoint!.DeclaringType!;

然后,我们将在

Program.cs
中找到我们之前创建的方法。

var hostBuilderFactory = entryPoint.GetMethod(
    "CreateHostBuilder",
    BindingFlags.Static | BindingFlags.NonPublic
)!;

我们将构造要传递给构建器的设置,将环境名称设置为名为

Environment
的变量(如下提供)。然后,我们使用这些设置调用工厂方法并将返回类型转换为
HostApplicationBuilder

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

我们调用一个名为

ConfigureHost
的可重写方法(如下所示),然后构建主机并启动它,确保不要阻塞等待主机完成的线程。

ConfigureHost(builder);
_host = builder.Build();
_host.StartAsync().GetAwaiter().GetResult();

完整课程如下:

public abstract class WorkerServiceFactory<T>
{
    private readonly IHost _host;

    protected WorkerServiceFactory()
    {
        var entryPoint = typeof(T).Assembly.EntryPoint!.DeclaringType!;
        var hostBuilderFactory = entryPoint.GetMethod(
            "CreateHostBuilder",
            BindingFlags.Static | BindingFlags.NonPublic
        )!;
        var settings = new HostApplicationBuilderSettings
        {
            ApplicationName = entryPoint.Assembly.GetName().Name,
            EnvironmentName = Environment
        };
        var builder = (HostApplicationBuilder)
            hostBuilderFactory.Invoke(null, new object?[] { settings })!;
        ConfigureHost(builder);
        _host = builder.Build();
        _host.StartAsync().GetAwaiter().GetResult();
    }

    protected virtual void ConfigureHost(HostApplicationBuilder builder) { }

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

    public IServiceProvider Services => _host.Services;

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

    private AsyncServiceScope? _serviceScope;
    private AsyncServiceScope ServiceScope => _serviceScope ??= Services.CreateAsyncScope();*/
}

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

剩下要做的就是创建您想要在测试中使用的工厂。为此,我们将从

WorkerServiceFactory
继承,并将我们的(现在公开的)
Program
作为程序集标记。

public sealed class MyWorkerFactory : WorkerServiceFactory<Program>
{
    protected override void ConfigureHost(HostApplicationBuilder builder)
    {
        // Configure your test services via builder.Services.
        // Configure your test services via builder.Configuration.
    }
}

您现在应该能够像使用

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

© www.soinside.com 2019 - 2024. All rights reserved.