havij
havij

Reputation: 1150

Unit testing an async method that uses System.Threading.Timer

In .NET Core, background tasks are implemented as IHostedService. This is my hosted service:

public interface IMyService {
    void DoStuff();
}

public class MyHostedService : IHostedService, IDisposable
{
    private const int frequency;

    private readonly IMyService myService;

    private Timer timer;

    public MyHostedService(IMyService myService, Setting s)
    {
        this.myService = myService;
        frequency = s.Frequency;
    }

    public void Dispose()
    {
        this.timer?.Dispose();
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        this.timer = new Timer(this.DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(this.frequency));
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        this.timer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        try
        {
            this.myService.DoStuff();
        }
        catch (Exception e)
        {
            // log
        }
    }
}

I am trying to unit test this class, and all I want is to make sure DoStuff gets called when StartAsync method is called. This is my unit test:

[TestFixture]
public class MyHostedServiceTests
{
    [SetUp]
    public void SetUp()
    {
        this.myService = new Mock<IMyService>();

        this.hostedService = new MyHostedService(this.myService.Object, new Setting { Frequency = 60 });
    }

    private Mock<ImyService> myService;
    private MyHostedService hostedService;

    [Test]
    public void StartAsync_Success()
    {
        this.hostedService.StartAsync(CancellationToken.None);

        this.myService.Verify(x => x.DoStuff(), Times.Once);
    }
}

Why is this failing?

Upvotes: 1

Views: 1192

Answers (1)

Nkosi
Nkosi

Reputation: 247018

It is failing because the async code is executing on a separate thread to the code that is verifying the expected behavior. That and the fact the the verifying code in invoked before the timer has had time to be invoked.

When testing an async method the test in most cases should also be async.

In this case you also need to let some time pass to allow the timer to invoke.

use Task.Delay to give the timer enough time to perform its function.

For example

[TestFixture]
public class MyHostedServiceTests {
    [SetUp]
    public void SetUp() {
        this.myService = new Mock<IMyService>();
        this.setting = new Setting { Frequency = 2 };
        this.hostedService = new MyHostedService(this.myService.Object, setting);
    }

    private Mock<ImyService> myService;
    private MyHostedService hostedService;
    private Setting setting;

    [Test]
    public async Task StartAsync_Success() {

        //Act
        await this.hostedService.StartAsync(CancellationToken.None);
        await Task.Delay(TimeSpan.FromSeconds(1));    
        await this.hostedService.StopAsync(CancellationToken.None);

        //Assert
        this.myService.Verify(x => x.DoStuff(), Times.Once);
    }
}

Above example uses a shorter frequency to test the expected behavior

Upvotes: 1

Related Questions