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 方法来说变得非常复杂。
有什么想法吗?谢谢🙂
大致基于微软的
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
一样使用它,减去一些您可以根据需要添加的更高级功能。