3

I'm aware that there are several similar questions on this and other websites, but the standard way of doing this doesn't appear to work in my situation for some reason. The normal way to accomplish this requirement is to use TaskScheduler.FromCurrentSynchronizationContext() as the TaskScheduler input parameter in the relevant Task.Factory.StartNew overload:

// Set uiTaskScheduler whilst on the UI thread
TaskScheduler uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
...
Task.Factory.StartNew(() => SomeMethodToRunAsynchronously(), 
    CancellationToken.None, TaskCreationOptions.None, uiTaskScheduler);

It appears that this is enough to schedule a Task to run on the UI thread, but it doesn't seem to work in my situation. In my case, I have a UiThreadManager class which among other things has a RunAsynchronously method in it:

public Task RunAsynchronously(Action method)
{
    return Task.Run(method);
}

This part works just fine. The problem that I face is that when running unit tests, this class is replaced with a MockUiThreadManager class (both implement an IUiThreadManager interface which is used by the application code) and I can't seem to force this method to run on the UI thread:

public Task RunAsynchronously(Action method)
{
    return Task.Factory.StartNew(() => method(), 
        CancellationToken.None, TaskCreationOptions.None, UiTaskScheduler);
}

The MockUiThreadManager class has a static UiTaskScheduler property which is set (like below) on the UI thread, so I assumed that all code passing through the above method would run as expected on that thread:

SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
MockUiThreadManager.UiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

However, when running a unit test, I noticed that it finished before the code that it was testing. I therefore added some breakpoints and called System.Threading.Thread.CurrentThread.ManagedThreadId in the Visual Studio Immediate Window at each breakpoint and sure enough, when the code passed through the above method, the thread ID changed.

So basically, I'm looking for a way to fake a Task based asynchronous call for the RunAsynchronously method that is run during unit tests that will ensure that the method actually runs synchronously on the UI thread. Does anybody see what I have done wrong, or have any other suggestions?

UPDATE >>>

Ok, I've used the wrong terminology here regarding the UI thread. Unit tests don't run on the UI thread because there's no UI... however, the situation remains the same. To clarify, I just need my tests to run the application code synchronously and on the main single thread that it starts on. The problem is that when the application is running, there is a lot of Task based asynchronous code and this needs to be tested via the MockUiThreadManager class synchronously.

Sheridan
  • 68,826
  • 24
  • 143
  • 183
  • This seems very counter intuitive to me. Typically when you run `Task.Factory.StartNew` you will be on the UI thread. – Aron Feb 04 '15 at 09:04
  • What is the value of `UiSynchronizationContext` when you pass it to `StartNew`? Show us how you're calling `Task.Factory.StartNew` in your test. – Yuval Itzchakov Feb 04 '15 at 09:11
  • *Typically when you run Task.Factory.StartNew you will be on the UI thread*... that's not at all true as it all depends on the current context. @YuvalItzchakov, did you mean `UiTaskScheduler`? I don't pass it to the `StartNew` method... as I said in my question, I initialise it on the UI thread... in Intellisense, it just says `Id=1`, so it's definitely not `null`. Also, I don't call `StartNew` in my test, I call the (`IMockUiThreadManager`) `RunAsynchronously` method which is shown above. – Sheridan Feb 04 '15 at 09:31
  • @Sheridan Have you considered simply executing the `Action` and using `Task.FromResult` to wrap it in a `Task`? – Yuval Itzchakov Feb 04 '15 at 09:54
  • @Sheridan: Wouldn't the technique involved in sharing the `SynchronizationContext` object be what you are looking for? I.e. invoke the test within the `SynchronizationContext` of the `IUiThreadManager`? – toadflakz Feb 04 '15 at 09:56
  • @YuvalItzchakov, either I have misunderstood your comment, or you seem to have totally misunderstood this question. I'll try to explain it again. Certain methods in my *application code* run `Task` based asynchronous code through the `UiThreadManager` class. My unit tests test these methods, but they are instead routed through the `MockUiThreadmanager` class. The `UiThreadManager` class runs the methods asynchronously, but I need the `MockUiThreadmanager` class to run them synchronously. – Sheridan Feb 04 '15 at 10:28
  • @toadflakz, that sounds like it would work, but how would I do that? The code from the answer to your question linked below doesn't help as I am not running any asynchronous code directly from the unit tests. – Sheridan Feb 04 '15 at 10:50
  • @Sheridan: Have you tried passing `TaskScheduler.FromCurrentSynchronizationContext()` instead of `UiTaskScheduler` in the `StartNew()` call in `MockUiThreadManager`? – toadflakz Feb 04 '15 at 11:29
  • @toadflakz, if you look at the last code excerpt in my question, you'll see that the `MockUiThreadManager.UiTaskScheduler` property is set to `TaskScheduler.FromCurrentSynchronizationContext()`, so yes, I have tried passing that... it still runs on its own separate thread. – Sheridan Feb 04 '15 at 11:44
  • @Sheridan: After looking much deeper, I've concluded Dirk is right and you will need to write a Thread Affinity SynchronizationContext as the UI SynchronizationContexts (whether Dispatcher or WinForms) use some form of queueing to implement the behaviour you experience on a UI thread. I have found one but I'm not sure what the consequences of using it will be (or if the code is safe or not) - http://wcf-examples.googlecode.com/svn/trunk/CodeRunner/ServiceModel.Extensions/ThreadAffinity/AffinitySynchronizer.cs – toadflakz Feb 04 '15 at 13:57

2 Answers2

4

After continuing my search, I have now found the solution that I was looking for and it can be achieved in just a few lines of code, so definitely no need to implement my own SynchronizationContext class. Looking at how simple it is, I'm surprised that I didn't find it earlier. In my MockUiThreadManager class that is used when running unit tests, I now have this code:

public Task RunAsynchronously(Action method)
{
    Task task = new Task(method);
    task.RunSynchronously();
    return task;
}

I can confirm that it does what it says and runs the method function synchronously on the same thread that the tests are run on.

For completeness sake, the Task.RunSynchronously Method also has an override that takes a TaskScheduler, but that was unnecessary in my case, so I won't need my MockUiThreadManager.UiTaskScheduler property anymore.

Sheridan
  • 68,826
  • 24
  • 143
  • 183
2

new SynchronizationContext() returns a new default SynchronizationObject which schedules work on the thread pool (reference source).

TaskScheduler.FromCurrentSynchronizationContext() returns a task scheduler that uses SynchronizationContext.Current which you just set to the thread pool sync context using SynchronizationContext.SetSynchronizationContext.

That means scheduling tasks on that scheduler will use the thread pool to execute them, and not a specific thread.

In general it is not even possible to schedule work on a specific thread, unless that thread has some sort of message queue. That's why you can schedule work to run on a UI thread.

I don't know which unit testing framework you use. It might have a UI to show the test results, but that doesn't mean that the tests are run on that thread.


I'm not sure how to solve this, other than writing you own SynchronizationContext class. It also feels a bit weird that you want to unit test something that has to run on the UI thread, because in my opinion the UI itself is something that's not very suited to unit tests. If you are using something like a ViewModel or a Controller then of course you can unit test those, but they should work regardless of the sync context you use.

Dirk
  • 10,668
  • 2
  • 35
  • 49
  • Sorry @Dirk, I shouldn't have used the term UI thread... I meant main thread... I just need the tests to run everything synchronously. – Sheridan Feb 04 '15 at 10:51