I've just seen a video-class by Jon Skeet, where he talks about unit testing asynchronous methods. It was on a paid website, but I've found something similar to what he says, in his book (just Ctrl+F "15.6.3. Unit testing asynchronous code").
The complete code can be found on his github, but I have simplified it for the sake of my question (my code is basically StockBrokerTest.CalculateNetWorthAsync_AuthenticationFailure_ThrowsDelayed()
but with TimeMachine and Advancer operations inlined).
Let's suppose we have a class to test a failed login (no unit test framework to simplify the question):
public static class LoginTest
{
private static TaskCompletionSource<Guid?> loginPromise = new TaskCompletionSource<Guid?>();
public static void Main()
{
Console.WriteLine("== START ==");
// Set up
var context = new ManuallyPumpedSynchronizationContext(); // Comment this
SynchronizationContext.SetSynchronizationContext(context); // Comment this
// Run method under test
var result = MethodToBeTested();
Debug.Assert(!result.IsCompleted, "Result should not have been completed yet.");
// Advancing time
Console.WriteLine("Before advance");
loginPromise.SetResult(null);
context.PumpAll(); // Comment this
Console.WriteLine("After advance");
// Check result
Debug.Assert(result.IsFaulted, "Result should have been faulted.");
Debug.Assert(result.Exception.InnerException.GetType() == typeof(ArgumentException), $"The exception should have been of type {nameof(ArgumentException)}.");
Console.WriteLine("== END ==");
Console.ReadLine();
}
private static async Task<int> MethodToBeTested()
{
Console.WriteLine("Before login");
var userId = await Login();
Console.WriteLine("After login");
if (userId == null)
{
throw new ArgumentException("Bad username or password");
}
return userId.GetHashCode();
}
private static Task<Guid?> Login()
{
return loginPromise.Task;
}
}
Where the implementation of ManuallyPumpedSynchronizationContext
is:
public sealed class ManuallyPumpedSynchronizationContext : SynchronizationContext
{
private readonly BlockingCollection<Tuple<SendOrPostCallback, object>> callbacks;
public ManuallyPumpedSynchronizationContext()
{
callbacks = new BlockingCollection<Tuple<SendOrPostCallback, object>>();
}
public override void Post(SendOrPostCallback callback, object state)
{
Console.WriteLine("Post()");
callbacks.Add(Tuple.Create(callback, state));
}
public override void Send(SendOrPostCallback d, object state)
{
throw new NotSupportedException("Synchronous operations not supported on ManuallyPumpedSynchronizationContext");
}
public void PumpAll()
{
Tuple<SendOrPostCallback, object> callback;
while(callbacks.TryTake(out callback))
{
Console.WriteLine("PumpAll()");
callback.Item1(callback.Item2);
}
}
}
The output is:
== START ==
Before login
Before advance
After login
After advance
== END ==
My question is: Why do we need the ManuallyPumpedSynchronizationContext
?
Why isn't the default SynchronizationContext enough? The Post()
method isn't even called (based on the output). I've tried commenting the lines marked with // Comment this
, and the output is the same and the asserts pass.
If I understood correctly what Jon Skeet says in the video, the SynchronizationContext.Post()
method should be called when we meet an await
with a not-yet-completed task. But this is not the case. What am I missing?
Aditional info
Through my researches, I stumbled across this answer. To try it, I changed the implementation of the Login()
method to:
private static Task<Guid?> Login()
{
// return loginPromise.Task;
return Task<Guid?>.Factory.StartNew(
() =>
{
Console.WriteLine("Login()");
return null;
},
CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}
With that modification, the Post()
method was indeed called. Output:
== START ==
Before login
Post()
Before advance
PumpAll()
Login()
After login
After advance
== END ==
So with Jon Skeet's use of TaskCompletionSource
, was his creation of ManuallyPumpedSynchronizationContext
not required?
Note: I think the video I saw was made just around C# 5 release date.