对使用 System.Threading.Timer 的异步方法进行单元测试

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

在 .NET Core 中,后台任务实现为 IHostedService。这是我的托管服务:

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
        }
    }
}

我正在尝试对此 class 进行单元测试,我只想确保在调用 StartAsync 方法时调用 DoStuff。这是我的单元测试:

[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);
    }
}

为什么会失败?

失败是因为异步代码在与验证预期行为的代码不同的线程上执行。那以及在计时器有时间被调用之前调用验证代码的事实。

在测试异步方法时,大多数情况下测试也应该是异步的。

在这种情况下,您还需要让一些时间过去以允许计时器调用。

使用 Task.Delay 给计时器足够的时间来执行其功能。

例如

[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);
    }
}

以上示例使用较短的频率来测试预期行为