问题
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.
- What is the best practice to write unit test for the
Async()
methods with mocking? - How to write unit test case for methods whose dependencies uses TPL ?(methods returning
Task
type)
Another question is
- 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);
来源:https://stackoverflow.com/questions/15207631/how-to-write-unit-test-cases-for-async-methods