单元测试异步代码

Unit testing asynchronous code

我有一个简单的 class,方法很简单:

partial class SimpleClass
{
    private readonly ISimpleManager _simpleManager;
    public SimpleClass(ISimpleManager simpleManager)
    {
        _simpleManager = simpleManager;
    }

    public async void SimpleMethod()
    {
        IsInProgress = true;
        DoSomeWork();
        Task<int> hardWork0Task = _simpleManager.DoHardWork0Async();
        Task<int> hardWork1Task = _simpleManager.DoHardWork1Async();
        DoIndependentWork();
        int hardWork0Result = await hardWork0Task.ConfigureAwait(false);
        DoDependentWork(hardWork0Result);
        int hardWork1Result = await hardWork1Task.ConfigureAwait(false);
        DoDependentWork(hardWork1Result);
        IsInProgress = false;
    }
}

让我们假设 属性 IsInProgress 只是 bool 属性 通知 GUI 它的状态以允许刷新进度条。 DoSomeWorkDoDependentWorkDoIndependentWork是一些使用或不用努力的方法。

在这种情况下,

ISimpleManager 是您可以想象的最简单的界面:

interface ISimpleManager
{
    Task<int> DoHardWork0Async();
    Task<int> DoHardWork1Async();
}

我必须使用 Moq 和 NUnit 编写一些单元测试。我该如何为这种情况编写单元测试?我想检查 IsInProgress 属性 的状态是否在整个代码 运行 期间与 GUI 异步更改为 false。是否有意义?可能吗?如果我的 async 方法 returns Task 或通用 Task<T> 怎么办?如果我配置等待 true 怎么办?

以下是单元测试 IsInProgress 设置是否正确的方法:

  1. 创建可等待信号量:

    var sem = new SemaphoreSlim(1);
    
  2. 使用以下实现创建存根 ISimpleManager

    async Task<int> DoHardWork0Async() {
       await sem.WaitAsync();
       return 0;
    }
    
  3. 致电SimpleMethod。它会卡在 DoHardWork0Async

  4. 调用中的信号量上
  5. 验证IsInProgress

  6. 的效果
  7. 作为清理步骤,释放信号量:

    sem.Release();
    

您只需要控制 ISimpleManager 任务何时完成。根据 Ilya 的回答,您可以使用 SemaphoreSlim,或者直接使用 TaskCompletionSource<T>。我通常更喜欢 TaskCompletionSource<T> 因为它更简单;但是,SemaphoreSlim 实例可以重复使用,而 TaskCompletionSource<T> 只能触发一次。

附带说明一下,您应该避免使用 async void,除非该方法是事件处理程序。它应该 是任何类型的 "default" - 默认应该是 return Task 除非你绝对 不能。所以在这个例子中,SimpleMethod当然应该returnTask.

这是使用 TCS 的样子:

async Task MyTestMethod()
{
  // Set up the mock object
  var tcs0 = new TaskCompletionSource<int>();
  var tcs1 = new TaskCompletionSource<int>();
  var stub = new Mock<ISimpleManager>();
  stub.Setup(x => x.DoHardWork0Async()).Returns(tcs0.Task);
  stub.Setup(x => x.DoHardWork1Async()).Returns(tcs1.Task);

  var sut = new SimpleClass(stub.Object);
  var task = sut.SimpleMethod();

  Assert.True(sut.InProgress);
  tcs0.SetResult(7);
  Assert.True(sut.InProgress);
  tcs1.SetResult(13);
  await task;
  Assert.False(sut.InProgress);
}