How can I write unit test for my background service?
Asked Answered
G

3

24

I'm working with the HostBuilder in .NET Core (not the WebHost !).

I have one Hosted Service running in my application that overrides the ExecuteAsync/StopAsync methods of the background Service and I want to unit test it.

Here is my HostedService:

public class DeviceToCloudMessageHostedService : BackgroundService
{
    private readonly IDeviceToCloudMessageService _deviceToCloudMessageService;
    private readonly AppConfig _appConfig;

    public DeviceToCloudMessageHostedService(IDeviceToCloudMessageService deviceToCloudMessageService, IOptionsMonitor<AppConfig> appConfig)
    {
        _deviceToCloudMessageService = deviceToCloudMessageService;
        _appConfig = appConfig.CurrentValue;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _deviceToCloudMessageService.DoStuff(stoppingToken);
            await Task.Delay(_appConfig.Parameter1, stoppingToken);
        }
    }
    
    public override Task StopAsync(CancellationToken cancellationToken)
    {
        Log.Information("Task Cancelled");
        _deviceToCloudMessageService.EndStuff();
        return base.StopAsync(cancellationToken);
    }

I already found this post: Integration Test for Hosted Service in .NET Core

But it's explained for a QueuedBackgroundService and I don't really know if I can test mine the same way.

I just want to know if my code is executed. I don't want any specific result. Do you have any idea of how I can test it?

Grefer answered 18/6, 2019 at 8:17 Comment(2)
You should be able to follow the same format. mock the dependencies and inject them, invoke the methods under test and assert the expected behaviorMccabe
Check this https://mcmap.net/q/583133/-net-core-3-worker-integration-testsElaineelam
M
28

You should still be able to follow a similar format as the linked answer.

Mock the dependencies and inject them, invoke the methods under test and assert the expected behavior.

The following uses Moq to mock the dependencies along with ServiceCollection to do the heavy lifting of injecting the dependencies.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

[TestMethod]
public async Task DeviceToCloudMessageHostedService_Should_DoStuff() {
    //Arrange
    IServiceCollection services = new ServiceCollection();
    services.AddSingleton<IHostedService, DeviceToCloudMessageHostedService>();
    //mock the dependencies for injection
    services.AddSingleton(Mock.Of<IDeviceToCloudMessageService>(_ =>
        _.DoStuff(It.IsAny<CancellationToken>()) == Task.CompletedTask
    ));
    services.AddSingleton(Mock.Of<IOptionsMonitor<AppConfig>>(_ =>
        _.CurrentValue == Mock.Of<AppConfig>(c => 
            c.Parameter1 == TimeSpan.FromMilliseconds(1000)
        )
    ));
    var serviceProvider = services.BuildServiceProvider();
    var hostedService = serviceProvider.GetService<IHostedService>();

    //Act
    await hostedService.StartAsync(CancellationToken.None);
    await Task.Delay(1000);//Give some time to invoke the methods under test
    await hostedService.StopAsync(CancellationToken.None);

    //Assert
    var deviceToCloudMessageService = serviceProvider
        .GetRequiredService<IDeviceToCloudMessageService>();
    //extracting mock to do verifications
    var mock = Mock.Get(deviceToCloudMessageService);
    //assert expected behavior
    mock.Verify(_ => _.DoStuff(It.IsAny<CancellationToken>()), Times.AtLeastOnce);
    mock.Verify(_ => _.EndStuff(), Times.AtLeastOnce());
}

Now, ideally this would count as testing framework code since you are basically testing that a BackgroundService behaves as expected when run, but it should demonstrate enough about how one would test such a service in isolation

Mccabe answered 18/6, 2019 at 10:48 Comment(4)
There are a couple of issues with this approach though. Firstly, you have time-based unit-tests which are inherently fragile. Secondly, you won't get meaningful exception messages out if something goes wrong - you will merely discover that your verify calls fail, and you'll need to step through with a debugger. A couple of better approaches: (1) Put as much of the service code into a testable class as possible, and inject an instance of that class into the BackgroundService. Then you only need to prove the injected class gets called (2) use reflection to call the ExecuteAsync method.Eatable
@Eatable If you have a better suggestion, why not make it an answer?Kaslik
hostedService.StartAsync is blocking, so your code does not work.Mclaurin
@Eatable if you were going to do with that approach I'd suggest using MediatR.Grote
B
3

The above solution is not work for me. Because the background service will run infinitely. My solution use CancellationToken and create a Thread to cancel it after a time . The code look like:

CancellationTokenSource source = new CancellationTokenSource();
CancellationToken token = source.Token;
new Thread(async () =>
{
     Thread.CurrentThread.IsBackground = true;
     await Task.Delay(500);
     hostedService.StopAsync(token);
}).Start();

await hostedService.StartAsync(token)
Boley answered 21/7, 2023 at 2:7 Comment(0)
C
1

You can create another service that inherited from your original service in your integration test project to execute protected methods of your BackGroundService.

  public class SpyService : MyOriginalBackGroundService
  {
  public bool WasExecuted { get; private set; } = false;

  public SpyDocumentCsvExportService(
      IOneOfMyCkass oneOfMyClass,
      ILog log)
      : base(oneOfMyClass,log) {}

  protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  {
      WasExecuted = true;
      await base.ExecuteAsync(stoppingToken);
  }}

And in your integration test class you can just call it after initializing it.

await spyService.StartAsync(CancellationToken.None);

and than be sure if it is executed:

 Assert.True(spyService.WasExecuted);
Carducci answered 6/3 at 8:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.