In this lesson, you will learn how to verify that background services perform their intended logic within an integration test harness using WebApplicationFactory.
Testing Background Service Tasks
When you register an IHostedService or BackgroundService in your ASP.NET Core application, it starts automatically when the host starts. Testing these in isolation is difficult because they are designed to run indefinitely. In an integration test, you don't want to spin up a real long-running task that blocks your test runner; instead, you want to verify that the service executes its unit of work when triggered.
Controlling the Host
To test a background service, use the WebApplicationFactory<TEntryPoint> to create a custom test server. The challenge is that the service starts immediately upon server creation. To gain control, you should use a "Trigger" or "Semaphore" pattern to synchronize your test with the background work.
Consider a service that processes a queue. Instead of letting it loop forever, you can inject a TaskCompletionSource or a Channel that the test can monitor:
public class WorkerIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public WorkerIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Swap the real repository with a mock to verify the action
services.AddSingleton<IMyRepository, MockRepository>();
});
});
}
[Fact]
public async Task Worker_ShouldProcessItem_WhenTriggered()
{
var client = _factory.CreateClient();
// Act: Perform an action that causes the BackgroundService to wake up
await client.PostAsync("/trigger-work", null);
// Assert: Give the background service a moment to finish its task
await Task.Delay(500);
var repo = _factory.Services.GetRequiredService<IMyRepository>();
Assert.True(repo.HasProcessedItem);
}
}
The Synchronization Strategy
Because BackgroundService runs on a background thread, your test will likely finish before the background task completes. Avoid Thread.Sleep in production code, but in integration tests, you can use a TaskCompletionSource injected into both the service and the test. The service sets the TaskCompletionSource.SetResult() once it completes its work, allowing your test to await the completion rather than guessing with an arbitrary delay.
Key Takeaway: To test background services, manipulate the service's dependencies to allow the test to observe or await the completion of the background task instead of relying on time-based delays.