- Motivation
- Requirements
- How to use
- Stop condition
- Use a time condition to stop the test server
- Configure a timeout for the condition set to stop the test server
- Configuring the interval of time on which the condition is checked
This extension allows you to do integration tests for your Background Services.
I want to be able to do integration tests as defined in introduction to integration tests but for scenarios that make use of Hosted Services.
When trying to do this you face 2 issues:
- The integration tests docs are made for web apps. If you just create a Background Service app and don't use a WebHost then there is no equivalent of
WebApplicationFactory
for doing integration tests against your host. - Using AAA terminology, how do you know when your act is done so that you can do your asserts ?
At the moment, the solution for this is to change your Host to a WebHost. If you don't do this at the moment you can't use the process described below for doing integration tests on Hosted Services.
By default on the Worker Service template, the IHost
instance is created as follows:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
To be able to use this testing extension you should change it to:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
And on the ConfigureServices method
of the Startup
class is where you add any Hosted Services you require:
public class Startup
{
private readonly IConfiguration _configuration;
public Startup(IConfiguration configuration)
{
_configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<Worker>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// add here any IApplicationBuilder configuration if required
}
}
The problem is that you only want to do your asserts when the Hosted Service has finished its work for your given test scenario. With this in mind, your basic test layout would be:
- Configure any mocks required and inject them in the test server
- Start the test server
- Wait for the Hosted Service to complete it's work for the given test case
- Stop the test server
- Do the asserts
Out of the box there is no way for you to know when the Hosted Service has finished the work. The simplistic solution is to always wait for a given period of time and then do the asserts. This is not the best solution because the wait times depend on the hardware on which the tests are run and usually leads to flaky tests.
The provided solution will let you do this on a custom condition or as well as on time based condition if that's what you actually require.
You will have to add the dotnet-sdk-extensions-testing nuget to your test project.
The dotnet-sdk-extensions-testing contains an IHost.RunUntilAsync
and a WebApplicationFactory.RunUntilAsync
extension methods. The RunUntilAsync
executes the host until a condition has been met:
- If you want to test a hosted service on a project which does not start a web server, like when using the
Worker Service
template, then follow the instructions at Test hosted service using IHost. - If you want to test a hosted service on a project which starts a web server, like when using the
ASP.NET Core Web API
template, then follow the instructions at Test hosted service using WebApplicationFactory.
When creating a project using the Worker Service
template, the IHost
instance is created and executed as follows:
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
})
.Build();
host.Run();
To run an integration test using the IHost
we need to create a wrapper around the IHostBuilder
so that it's shared between your app and the tests and so that we can mock dependencies if needed.
You can do this however you like, the following shows a simple approach:
- Create a wrapper class that exposes the
IHostBuilder
.
public class WorkerHostBuilder
{
public WorkerHostBuilder(params string[] args)
{
Builder = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
});
}
public IHostBuilder Builder { get; }
}
- Change your
Program.cs
to use the wrapper class.
var workerHostBuilder = new WorkerHostBuilder(args);
var host = workerHostBuilder.Builder.Build();
host.Run();
- Create a test where you use the wrapper class to build the
IHost
.
[Fact]
public async Task DemoTest()
{
var workerHostBuilder = new WorkerHostBuilder()
.Builder
.ConfigureServices(services =>
{
services.AddSingleton<ICalculator>(calculatorMock);
});
var host = workerHostBuilder.Build();
await host.RunUntilAsync(() => <some condition>);
// do some asserts
}
The above are the basic steps to do a test. Let's imagine that our Hosted Service will be modified to do the following:
public interface ICalculator
{
int Sum(int left, int right);
}
public class Calculator : ICalculator
{
public int Sum(int left, int right)
{
return left + right;
}
}
public class Worker : BackgroundService
{
private readonly ICalculator _calculator;
public Worker(ICalculator calculator)
{
this._calculator = calculator;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_calculator.Sum(1, 1);
await Task.Delay(300, stoppingToken);
}
}
}
The difference from the default Worker Service
template is that we added a dependency of type ICalculator
and made the Hosted Service invoke the ICalculator.Sum
method.
Now let's say that we want to make a test to the Hosted Service. We could say for instance that we want to run our test until the ICalculator.Sum
method was called 3 times and then do some asserts. We can do that as follows:
- Make sure the
ICalculator
dependency is added to the wrapper class that exposes theIHostBuilder
. Without this the host service would fail to run because it would be able to provide an instance ofICalculator
to the Hosted Service.
public class WorkerHostBuilder
{
public WorkerHostBuilder(params string[] args)
{
Builder = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
services.AddSingleton<ICalculator, Calculator>();
});
}
public IHostBuilder Builder { get; }
}
- Override the
ICalculator
dependency so that we can use it as a condition for stopping our test when theICalculator.Sum
is called 3 times.
[Fact]
public async Task DemoTest2()
{
var callCount = 0;
var calculatorMock = Substitute.For<ICalculator>(); // using NSubstitute for mocking but you can use whatever you prefer
calculatorMock
.Sum(Arg.Any<int>(), Arg.Any<int>())
.Returns(1)
.AndDoes(info => callCount++); // keep count of how many times the ICalculator.Sum method has been called
var workerHostBuilder = new WorkerHostBuilder()
.Builder
.ConfigureServices(services =>
{
services.AddSingleton<ICalculator>(calculatorMock);
});
var host = workerHostBuilder.Build();
await host.RunUntilAsync(() => callCount >= 3); // stop host execution when the ICalculator.Sum method has been called 3 times
// do some asserts
}
The above is a very simple example that hopefully gives you an idea on how you can do the integratin style tests for Hosted Services.
Note
You can also consider converting your project to start a web application and follow the instructions at Test hosted service using WebApplicationFactory.
To convert to a project that starts a web application use a template like the ASP.NET Core Web API
and migrate your code accross. You can take manual steps to convert a project that doesn't start a web application, like one that uses a Worker Service
template, but it's probably easier to do it by creating a new project and moving the code across to it.
If you already have a project which starts a web application, like the one you get from using the ASP.NET Core Web API
template, then you should start by creating an integration test as shown in introduction to integration tests.
The type of the generic used in WebApplicationFactory<T>
needs to be a class from your project. You can follow the documentation on the official examples to expose the Program
type so that it can be used in WebApplicationFactory<Program>
or you can make the Hosted Service public
and use that as the generic type on WebApplicationFactory<T>
. The example below uses the latter.
For demo purposes let's assume that the Hosted Service is going to execute the method ICalculator.Sum
on a loop. Let's also say that we want to make a test to the Hosted Service where we want to run the test until the ICalculator.Sum
method was called 3 times and then do some asserts. We can do that as follows:
- Declare the
ICalculator
type and a type that implements it.
public interface ICalculator
{
int Sum(int left, int right);
}
public class Calculator : ICalculator
{
public int Sum(int left, int right)
{
return left + right;
}
}
- Register the
ICalculator
type in the service collection. On theProgram.cs
:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<Worker>(); // the hosted service
builder.Services.AddSingleton<ICalculator, Calculator>(); // register the ICalculator type
- Update the Hosted Service to call the
ICalculator.Sum
:
public class Worker : BackgroundService
{
private readonly ICalculator _calculator;
public Worker(ICalculator calculator)
{
this._calculator = calculator;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_calculator.Sum(1, 1);
await Task.Delay(300, stoppingToken);
}
}
}
- Update the test to mock the
ICalculator
type and set a condition to stop the host when theICalculator.Sum
has been called 3 times:
public class HostedServiceDemoTests : IClassFixture<WebApplicationFactory<Worker>>
{
private readonly WebApplicationFactory<Worker> _webApplicationFactory;
public HostedServiceDemoTests(WebApplicationFactory<Worker> webApplicationFactory)
{
_webApplicationFactory = webApplicationFactory;
}
[Fact]
public async Task DemoTest()
{
var callCount = 0;
var calculatorMock = Substitute.For<ICalculator>(); // using NSubstitute for mocking but you can use whatever you prefer
calculatorMock
.Sum(Arg.Any<int>(), Arg.Any<int>())
.Returns(1)
.AndDoes(info => callCount++); // keep count of how many times the ICalculator.Sum method has been called
await _webApplicationFactory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton<ICalculator>(calculatorMock);
});
})
.RunUntilAsync(() => callCount >= 3); // stop host execution when the ICalculator.Sum method has been called 3 times
// do some asserts
}
}
The above is a very simple example that hopefully gives you an idea on how you can do the integratin style tests for Hosted Services.
The main difference from the integration test examples shown in introduction to integration tests is that you do not use the WebApplicationFactory.CreateClient()
and then use the returned HttpClient do to calls into the test server but instead you use the WebApplicationFactory.RunUntilAsync
extension method with a custom conditions that will control the lifetime of the test server when using Hosted Services.
Note
When thinking about your test scenario understand that your code running on your Hosted Service won't immediatly stop when the test condition is reached. In reality, the set condition is checked periodically to understand if the test server should be stopped.
To put it another way, the set condition actually means don't stop the server before at least this condition is met.
This is important when planning your stop condition and asserts as it might mean that more of your code executed than you might initially think if you don't plan your stop condition appropriately.
As an example if your Hosted Service is in a while loop doing some operation and you are keeping count of how many times that operation has run before stopping the test server, then the stop condition should probably be numberOfRuns >= 'some value'
instead of numberOfRuns == 'some value'
.
If you prefer to run the web server for a period of time before terminating it you can use the WebApplicationFactory.RunUntilTimeoutAsync
extension method:
Given that you have an instance of WebApplicationFactory
you can do someting like:
await _webApplicationFactory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
// inject mocks for any other services
});
})
.RunUntilTimeoutAsync(TimeSpan.FromSeconds(3));
Usually it's best to consider stopping after a stop condition is met. Abusing the WebApplicationFactory.RunUntilTimeoutAsync
and using it in scenarios where you could have set a stop condition using the WebApplicationFactory.RunUntilAsync
might lead to flaky tests.
When setting a condition to for the WebApplicationFactory.RunUntilAsync
extension method there is a default timeout of 5 seconds set to reach that condition. If the condition is not reached the test server is stopped and a RunUntilException
is thrown.
This is to avoid having a test that never ends because the set condition is never reached. You can configure this timeout by using an overload of WebApplicationFactory.RunUntilAsync
as follows:
await _webApplicationFactory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton<ICalculator>(someMock);
});
})
.RunUntilAsync(() => callCount == 3, options => options.Timeout = TimeSpan.FromMilliseconds(100));
The above changes the default 5 seconds timeout to 100 milliseconds.
Note that when debugging (Debugger.IsAttached
is true) the default timeout will not be 5 seconds, it will instead be 1 day. This is done so that you can take your time when debugging tests and not have the timeout being triggered and abort the test server in the middle of debugging.
The above is only true for the default timeout. Meaning that any timeout that you set is honored even when debugging.
Beware of this when you're debugging tests where you've set a low timeout. You might have to increase your set timeout to something large enough to let you debug your test properly and then once you're happy set it back to the desired timeout.
When you set a condition, that condition is checked in a loop until it's reached or until the timeout is triggered:
- if it evaluates to true the server is stopped
- if it evaluates to false the condition is only checked after some time
By default the condition is checked in intervals of 5 milliseconds. This can be configured by using the an overload of WebApplicationFactory.RunUntilAsync
extension method as follows:
await _webApplicationFactory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton<ICalculator>(someMock);
});
})
.RunUntilAsync(() => callCount == 3, options => options.PredicateCheckInterval = TimeSpan.FromMilliseconds(100));
Setting the RunUntilOptions.PredicateCheckInterval
to high values might mean your test takes longer to finish because one way one thinking about this setting is that it represents the longest time possible between your condition evaluating to true and the server being given the order to stop.
So if for your test it will take X time to meet the condition and the RunUntilOptions.PredicateCheckInterval
is represented by Y than in the worst case scenario the time to run your test will be close to X + Y.
[!NOTE]: when debugging it might be useful to set the RunUntilOptions.PredicateCheckInterval
to a larger period to allow you to step through your code more easily before the check for the condition kicks in and, if evaluates to true, shuts down the test server and ends the test.