如何为异步方法编写单元测试用例?

3
我希望能通过模拟依赖项来编写单元测试用例。总体流程如下。
我们有一个名为 WorklistLoader 的类,其中包含一个异步方法 LoadWorklistItemsAsync()。为了完成这项任务,WorklistLoader 依赖于较低层的 API(我想要模拟的),即 QueryManager.StartQueryTask()StartQueryTask() 同样是一个异步方法,它会查询文件系统并以固定间隔触发 ProgressChanged(),最终触发 CompletedEventStartQueryTask() 返回对 TPL Task 的引用。 StartQueryTask 的签名为:
Task StartQueryTask(
    "SomeId",
    EventHandler<ProgressChanged> progressChanged,
    EventHandler<QueryCompleted> queryCompleted);

一旦WorklistLoaderQueryManager接收到ProgressChanged事件,它会进行一些处理,然后触发其自己的ProgressChanged事件(ViewModel已经订阅了该事件)。

我想用模拟QueryManager.StartQueryTask()来测试WorklistLoaderLoadWorklistItemsAsync()方法。

以下是我的问题:

  1. 使用模拟,如何编写异步(Async())方法的单元测试的最佳实践?
  2. 如何为依赖于TPL的方法编写单元测试用例?(返回Task类型的方法)

另一个问题是:

  1. 如果我使用Rhinomocks来模拟QueryManager.StartQueryTask()方法,它会是什么样子?(模拟代码。它必须引发progresschangedcompleted事件并返回Task

你是在使用模拟框架,还是手动实现接口/子类化? - Gary Johnson
问题1实际上与异步无关,只是模拟。例如,您必须以某种方式将模拟注入到“WorklistLoader”中,无论最终使用模拟的方法是异步还是同步。至于问题2,我建议您查看http://www.srtsolutions.com/testing-async-methods-in-c-5 - Peter Ritchie
2个回答

1

为了进行模拟,您需要能够将模拟注入到您正在使用的任何地方。有许多方法可以做到这一点,例如使用控制反转容器、环境上下文引导代码等。最简单的方式是使用构造函数注入并引导您的环境上下文,以在测试时具有所需的模拟。例如:

WorklistLoader worklistLoader;

[SetUp]
public void Setup()
{
    worklistLoader = new WorklistLoader(new MockQueryManager());
}

[Test]
public async Task TestWorklistLoader()
{
    await worklistLoader.LoadWorklistItemsAsync();
}

这也意味着WorklistLoader不依赖于QueryManager,而是依赖于像IQueryManager这样的抽象,MockQueryManager将实现它。
其中MockQueryManager可能是类似于:
public class MockQueryManager : IQueryManager
{
    public Task StartQueryTask() {/* TODO: */}
}

当然,你的原始QueryManager必须实现IQueryManagear接口:

public class QueryManager : IQueryManager
{
    public Task StartQueryTask() {/* TODO: */}
}

现在,就测试使用TPL的类而言,你会注意到我实现了一个返回Task的异步测试方法。这告诉测试运行器在认为测试方法已执行之前等待结果。如果你只是简单地编写以下内容:
[Test]
public async void TestWorklistLoader()
{
    await worklistLoader.LoadWorklistItemsAsync();
}

这位运行者将执行TestWorklistLoader,在LoadWorklistItemsAsync完成之前立即返回,并可能绕过任何断言。

更新:

如果您不使用C# 5,则建议在单元测试中等待任务完成。例如:
[Test]
public void TestWorklistLoader()
{
    var task = worklistLoader.LoadWorklistItemsAsync();
    if(!task.IsComplete()) task.Wait();
}

我没有使用C#5.0功能的奢侈条件。但我知道可以用更多的代码来实现相同的功能。这引出了另一个问题。如果我使用Rhinomocks模拟QueryManager,那么我应该怎么返回Task类型?我的模拟方法会是什么样子? - Prashanth R
你需要告诉RhinoMocks返回什么,例如 var theTask = CreateTask(); MockRepository.GenerateMock<IQueryManager>().Stub(qm=>StartQueryTask()).Return(theTask); - Peter Ritchie
我已经为非C#5添加了一些细节。 - Peter Ritchie

0

这可能看起来有些简陋,但我在类似测试构建场景中采取的不太复杂的方法是使用这个方便的函数:

/// <summary>
/// Wait no longer than @waitNoLongerThanMillis for @thatWhatWeAreWaitingFor to return true.
/// Tests every second for the 
/// </summary>
/// <param name="thatWhatWeAreWaitingFor">Function that when evaluated returns true if the state we are waiting for has been reached.</param>
/// <param name="waitNoLongerThanMillis">Max time to wait in milliseconds</param>
/// <param name="checkEveryMillis">How often to check for @thatWhatWeAreWaitingFor</param>
/// <returns></returns>
private bool WaitFor(Func<bool> thatWhatWeAreWaitingFor, int checkEveryMillis, int waitNoLongerThanMillis)
{
    var waitedFor = 0;
    while (waitedFor < waitNoLongerThanMillis)
    {
        if (thatWhatWeAreWaitingFor()) return true;

        Console.WriteLine("Waiting another {0}ms for a situation to occur.  Giving up in {1}ms ...", checkEveryMillis, (waitNoLongerThanMillis - waitedFor));
        Thread.Sleep(checkEveryMillis);
        waitedFor += checkEveryMillis;
    }
    return false;
}

使用方法:

// WaitFor (transaction to be written to file, checkEverySoOften, waitNoLongerThan)
int wait = (Settings.EventHandlerCoordinatorNoActivitySleepTime + 5) * 1000;
var fileExists = WaitFor(() => File.Exists(handlerConfig["outputPath"]), checkEveryMillis: 1000, waitNoLongerThanMillis: wait);

if(!fileExists)
     Assert.Fail("Waited longer than " + wait + " without any evidence of the event having been handled.  Expected to see a file appear at " + handlerConfig["outputPath"]);

在我的场景中,我期望文件被写入,这就是我等待的。在你的情况下,你正在等待progressChanged和queryCompleted被调用,所以你最好注入那些 Mocks,并且你等待的表达式应该是真的:
var eventsCalled = WaitFor(() => progressChanged.Called(Time.Once) && queryCompleted.Called(Times.Once), checkEveryMillis: 1000, waitNoLongerThanMillis: wait);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接