【问题标题】:How to write unit test cases for async methods?如何为异步方法编写单元测试用例?
【发布时间】:2013-03-04 17:42:04
【问题描述】:

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

我们有一个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() 方法,它会是什么样子? (模拟代码。它必须引发progresschanged、completed 事件并返回Task)。

【问题讨论】:

  • 您是在使用模拟框架,还是只是手动实现接口/子类化自己?
  • 问题 1 真的与异步没有任何关系,只是模拟。例如无论使用模拟的最终方法是异步还是同步,您都必须以某种方式将模拟注入WorklistLoader。至于2,我建议你看看srtsolutions.com/testing-async-methods-in-c-5

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


【解决方案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 的类方面,您会注意到我已经实现了一个返回任务的异步测试方法。这告诉测试运行者在认为测试方法已经执行之前等待结果。如果你只是写了这样的东西:

[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 那么我应该怎么做才能返回任务类型?我的模拟方法会是什么样子?
  • 你必须告诉 RhinoMocks 返回什么例如var theTask = CreateTask(); MockRepository.GenerateMock&lt;IQueryManager&gt;().Stub(qm=&gt;StartQueryTask()).Return(theTask);
  • 我为非 C#5 添加了一些细节
【解决方案2】:

这可能看起来有点古怪,但我对类似的测试构建场景采取的简单方法是使用这个方便的功能:

/// <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);

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-14
    相关资源
    最近更新 更多