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

时间:2013-03-04 17:42:05

标签: .net unit-testing nunit rhino-mocks

我想通过模拟依赖项来编写单元测试用例。整体流程如下。

我们有一个WorklistLoader,它有一个异步方法LoadWorklistItemsAsync()。要完成此任务,WorklistLoader依赖于较低层API(我想模拟)QueryManager.StartQueryTask()StartQueryTask()也是一种异步方法,用于查询文件系统并定期引发ProgressChanged(),然后在最后引发CompletedEventStartQueryTask()会返回对TPL​​ Task的引用。

StartQueryTask的签名是

Task StartQueryTask(
    "SomeId",
    EventHandler<ProgressChanged> progressChanged,
    EventHandler<QueryCompleted> queryCompleted);

WorklistLoader收到来自ProgressChanged的{​​{1}}事件后,会进行一些处理,然后引发QueryManager事件ProgressChanged订阅的事件)。

我想使用模拟ViewModel来测试LoadWorklistItemsAsync()的{​​{1}}方法。

以下是我的问题。

  1. 使用模拟为WorklistLoader方法编写单元测试的最佳做法是什么?
  2. 如何为依赖项使用TPL的方法编写单元测试用例?(返回QueryManager.StartQueryTask()类型的方法)
  3. 另一个问题是

    1. 如果我使用Rhinomocks模拟我的QueryManager.StartQueryTask()方法它会是什么样子? (模拟代码。它必须提高进度,完成事件并返回任务)。

2 个答案:

答案 0 :(得分:1)

为了模拟某些东西,你需要能够将模拟注入到你正在使用的任何东西中。有许多方法可以实现这一点,包括Inversion of Control容器,环境上下文引导代码等。最简单的方法是构造函数注入和引导环境上下文,以便在您想要测试时获得所需的模拟。例如:

WorklistLoader worklistLoader;

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

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

这也意味着WorklistLoader不依赖于QueryManager,而是依赖IQueryManager MockQueryManager所实现的抽象,如<{1}}。

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

答案 1 :(得分: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);