4

We´re planning to use async/await in our MVVM view models, but hit a hard issue with unit testing this code. When using NUnit and a hand written mock for our messaging we´re losing the current SynchronizationContext.

Best shown with following small reproducing example code:

[Test] public void TestMethod()
{       
  Func<Task> asyncMethod = async () =>
    {
      var context = SynchronizationContext.Current;
      await TaskEx.Yield();
      Assert.AreEqual(context, SynchronizationContext.Current);
    };

    // Establish the new context
    var syncCtx = new SingleThreadSynchronizationContext(false);
    SynchronizationContext.SetSynchronizationContext(syncCtx);

    // Invoke the function and alert the context to when it completes
    var t = asyncMethod();
    t.ContinueWith(delegate { syncCtx.Complete(); }, TaskScheduler.Default);

    // Pump continuations and propagate any exceptions
    syncCtx.RunOnCurrentThread();
    t.GetAwaiter().GetResult();
}

Actually most of this code is stolen from the AsyncPump implementation from Stephen Toub on his blog.

Interestling all needed to make this test pass is tossing in a ExecutionContext.SuppressFlow(); before calling the async method. This could be enough to fix our Problem, but i do not know enough about ExecutionContext and i want some deeper understanding what´s going on.

Why does the code generated by the await statement swallow the current SynchronizationContext?
Is there another obvious way in using a single threaded context for unit testing async/await code?

PS: We´re using .Net4 and Microsoft.CompilerServices.AsyncTargetingPack.Net4

PPS: This also occurs in a simple project using the stable Microsoft.Bcl.Async instead of the ATP

sanosdole
  • 2,469
  • 16
  • 18
  • Please upgrade to `Microsoft.Bcl.Async`. – Stephen Cleary Apr 19 '13 at 17:09
  • I see that the ATP is outdated, do you think this is a bug with it and fixed in the new Version? I just can´t easily change the Version right now, maybe Monday... – sanosdole Apr 19 '13 at 17:16
  • 1
    I'm not sure, but the ATP is a very old version. – Stephen Cleary Apr 19 '13 at 17:22
  • 1
    I believe in a unit test the handling of the synchronization context would be different than that of a UI. I'm not sure what you're testing is actually going to tell you anything useful. e.g. in a test there is no "message pump" to marshal back to, so the framework doesn't necessarily know how to inject execution back into that original synchronization context. – Peter Ritchie Apr 19 '13 at 17:37
  • @PeterRitchie, he's setting his own `SynchronizationContext` (the `SingleThreadSynchronizationContext`). If you read the linked article, you'll see that this creates its own pump. This can come in handy when (a) you'd like a way to get to the main thread, but (b) you don't want to pump windows messages (see http://stackoverflow.com/a/14820744/495262) – Matt Smith Apr 20 '13 at 03:11
  • @PeterRitchie This code just reproduces the problem. We want to test ViewModel code using async/await. So we need to simulate the message pump. We want to test things like that a disabled flag set during async operation. – sanosdole Apr 21 '13 at 09:50
  • 2
    An upgrade to Microsoft.Bcl.Async did not help. – sanosdole Apr 23 '13 at 08:09

2 Answers2

1

You are running into a bug in .NET 4.0 which is fixed in .NET 4.5:

SynchronizationContext.Current is null in Continuation on the main UI thread

It is the same issue, since the code after the await will be wrapped in a continuation.

Community
  • 1
  • 1
Matt Smith
  • 17,026
  • 7
  • 53
  • 103
  • Those issues seem related, but i´m not quite convinved. We get a SynchronizationContext.Current. Just the wrong one. It has to do something with how the SynchronizationContext is flowed with the ExecutionContext. I´ll do an isolated test outside of our solution to see how it turns out with different environments. – sanosdole Apr 21 '13 at 09:57
  • What is the type of the one you get? – Matt Smith Apr 22 '13 at 01:32
  • Some good reading I came across while investigating: http://blogs.msdn.com/b/pfxteam/archive/2012/06/15/executioncontext-vs-synchronizationcontext.aspx I'm stumped as to why you'd have the default SynchronizationContext set as the current (unless the Test framework sets it somewhere). Perhaps setting a breakpoint on Synchronizationcontext.Setsynchronizationcontext will help you find out who is setting the default one to the current one in the first place. – Matt Smith Apr 22 '13 at 16:39
  • I believe it is set by the flowed ExecutionContext. But i´ll verify this. – sanosdole Apr 23 '13 at 08:12
  • Well it seems very similiar to your issue. The context is set from ExecutionContext.Run, it only occurs under .Net 4.0 and is fixed in .Net 4.5. The only difference is that we get an ThreadPool Context instead of null... – sanosdole Apr 23 '13 at 10:50
  • @sanosdole, It seems the same--the difference seems to be that the ExecutionContext that gets swapped in has the default SynchronizationContext on it. When, typically, without doing anything special, the ExecutionContext's SynchronizationContext would be null. – Matt Smith Apr 23 '13 at 13:06
1

I had exactly same problem.

I found out it is because my custom SynchronizationContext didn't properly override and implement CreateCopy method. It seems the async code creates copy of the context after every task (or something). Make sure yours implements it properly too.

Euphoric
  • 12,645
  • 1
  • 30
  • 44
  • That helps, although I still do not really understand the whole issue. Also we may not override the NUnit context. Really time for updating... – sanosdole Jun 28 '16 at 11:06