How to write unit test cases for async methods?

2019-05-13 22:55发布

问题:

I would like to write a unit test case by mocking the dependencies. The overall flow is as follows.

We have a WorklistLoader which has an async method LoadWorklistItemsAsync(). To accomplish this task WorklistLoader is dependent on lower layer API(which i want to mock) QueryManager.StartQueryTask(). StartQueryTask() is also a async method which queries the file system and raises the ProgressChanged() at regular intervals and then at the end raises the CompletedEvent. StartQueryTask() returns a reference to TPL Task.

Signature of the StartQueryTask is

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

Once the WorklistLoader recieves the ProgressChanged event from QueryManager, it does some processing and then raises its ProgressChanged event(to which the ViewModel has subscribed).

I would like to test LoadWorklistItemsAsync() method of the WorklistLoader with mocking QueryManager.StartQueryTask().

Here are my questions.

  1. What is the best practice to write unit test for the Async() methods with mocking?
  2. How to write unit test case for methods whose dependencies uses TPL ?(methods returning Task type)

Another question is

  1. If i mock my QueryManager.StartQueryTask() method using Rhinomocks How would it look like ? (The mocking code. It has to raise progresschanged, completed events and return Task).

回答1:

In order to mock something, you need to be able to inject the mock into whatever you are using. There are many ways you can do this, with Inversion of Control containers, ambient context bootstrap code, etc. The easiest way is to constructor injection and bootstrap your ambient context to have the mock you want when you want to test. For example:

WorklistLoader worklistLoader;

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

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

This also means that WorklistLoader doesn't depend on QueryManager but depends on an abstraction like IQueryManager that MockQueryManager would implement.

Where MockQueryManager might be something like:

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

And of course your original QueryManager would have to implement IQueryManagear:

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

Now, in terms of testing TPL-using classes, you'll notice that I've implemented an async test method that returns a Task. This tells test runners to wait for the result before thinking the test method has executed. If you simply wrote something like:

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

The runner would execute TestWorklistLoader and it would return immediately before LoadWorklistItemsAsync completed and possibly bypass any asserts.

Update:

If you're not using C# 5, then I'd recommend simply waiting for the task to complete within the unit test. For example:

[Test]
public void TestWorklistLoader()
{
    var task = worklistLoader.LoadWorklistItemsAsync();
    if(!task.IsComplete()) task.Wait();
}


回答2:

This may seem rinky-dink, but the unsophisticated approach I've taken to similar test-construction scenarios is to use this handy function:

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

Usage:

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

In my scenario I'm expecting a file to be written so that is what I wait for. In your case you are waiting for progressChanged and queryCompleted to be called so you would do well to inject Mocks of those and the expression you are waiting to be true is:

var eventsCalled = WaitFor(() => progressChanged.Called(Time.Once) && queryCompleted.Called(Times.Once), checkEveryMillis: 1000, waitNoLongerThanMillis: wait);