I'm trying to unit test some behavior in my application that uses Prism's event aggregator. One of the things the code I'm trying to unit test does is subscribe to events on the UI thread. Digging into EventAggregator's implementation, I found that it does so via a SynchronizationContext.Post
.
I thought this answer might be a good workaround, but I ended up using a more simple fix: explicitly setting the sync context at the beginning of the unit test - which works until you attempt to read SynchronizationContext.Current
Which lead me to a behavior I don't entirely understand:
//set the sync context
var thisSyncContext = new SynchronizationContext();
SynchronizationContext.SetSynchronizationContext(thisSyncContext);
thisSyncContext.Post(cb => {
var ctx = SynchronizationContext.Current; //<-- this is null
var equals = thisSyncContext.Equals(ctx); //<-- this is false
},null);
thisSyncContext.Send(cb => {
var ctx = SynchronizationContext.Current; //<-- this is not null
var equals = thisSyncContext.Equals(ctx); //<-- this is true
}, null);
I understand that a Post happens asynchronously and a Send happens synchronously, and when I watch it in the thread debug window, it actually kicks over to a different thread ID, as you would expect an async call to do.
I guess what I'm trying to understand is, when I tell a synchronization context to execute a function, whether synchronously or asynchronously, I would expect that context to be preserved. It is preserved for synchronous calls, but not for async.
Why is this behavior exhibited, and how can I compensate for it in my unit tests?
Ok. So I think I figured this out, with a lot of help from this article.
If you look at the source for EventAggregator, When you Publish
using ThreadOption.UiThread, you're telling SynchronizationContext.Current
to Post
.
When running in a WPF application, SynchronizationContext.Current
is an instance of a DispatcherSynchronizationContext
, whose implementation of Post asynchronously kicks us over back to the original UI thread, as we would expect it to..
In my example (and my unit tests), I'm not using a DispatcherSynchronizationContext
- I'm using a plain-jane SynchronizationContext
, whose default implementation of Post makes a call to ThreadPool.QueueUserWorkItem
. That's kind of a confusing default implementation given the documentation - it really probably should be an abstract method.
Anyway, this implementation spawns a new thread, and the new thread gets a new ExecutionContext, and that execution context's synchronization context, by default, is null.
I guess the neat thing to note here is that Prism doesn't care what type the sync context is - it just needs a reference to exist the first time it accesses it when the EventAggregator is resolved.
So the solution here is to make our own synchronization context, which replaces the intended async behavior with synchronous behavior.
/// <summary>
/// Prism's UI thread option works by invoking Post on the current synchronization context.
/// When we do that, base.Post actually looses SynchronizationContext.Current
/// because the work has been delegated to ThreadPool.QueueUserWorkItem.
/// This implementation makes our async-intended call behave synchronously,
/// so we can preserve and verify sync contexts for callbacks during our unit tests.
/// </summary>
internal class MockSynchronizationContext : SynchronizationContext
{
public override void Post(SendOrPostCallback d, object state)
{
d(state);
}
}
For purposes of my unit tests, I don't need async responsiveness for event publishing, but I do need to verify that subscriptions intended for the UI thread execute on the thread which started the unit test.
And now, when we run the following code:
//set the sync context
var thisSyncContext = new MockSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(thisSyncContext);
thisSyncContext.Post(cb => {
var ctx = SynchronizationContext.Current; //<-- this is not null
var equals = thisSyncContext.Equals(ctx); //<-- this is true
},null);
thisSyncContext.Send(cb => {
var ctx = SynchronizationContext.Current; //<-- this is not null
var equals = thisSyncContext.Equals(ctx); //<-- this is true
}, null);