-1

I'm unit testing a part of my WPF project that uses async code and Application.Current.MainWindow to set the title of the main window.

public class ClassUnderTest
{
    // Get and apply title async
    public async Task GetAndApplyWindowTitleFromDbAsync()
    {
        string windowTitle = await GetWindowTitleFromDbAsync();
        Application.Current.MainWindow.Title = windowTitle;
    }

    public async Task<string> GetWindowTitleFromDbAsync()
    {
        await Task.Delay(2000);
        return "Async Window Title";
    }

    // Get and apply title sync
    public void GetAndApplyWindowTitleFromDb()
    {
        Application.Current.MainWindow.Title = "Sync Window Title";
    }
}

The unit test with the synchronous method succeeds while the async method throws the following exception when accessing Application.Current.MainWindow after the await:

System.InvalidOperationException: The calling thread cannot access this object because a different thread owns it.

[TestClass]
public class TestClass
{

    [TestMethod]
    public void SyncMethodWithUICodeTest()
    {
        InitializeApplication();

        Assert.AreEqual("Window Title", Application.Current.MainWindow.Title);

        var classUnderTest = new ClassUnderTest();

        classUnderTest.GetAndApplyWindowTitleFromDb();

        // Test succeeds
        Assert.AreEqual("Sync Window Title", Application.Current.MainWindow.Title);
    }

    [TestMethod]
    public async Task AsyncMethodWithUICodeTest()
    {
        InitializeApplication();

        Assert.AreEqual("Window Title", Application.Current.MainWindow.Title);

        var classUnderTest = new ClassUnderTest();

        await classUnderTest.GetAndApplyWindowTitleFromDbAsync();

        // throws InvalidOperationException
        Assert.AreEqual("Async Window Title", Application.Current.MainWindow.Title);
    }

    private void InitializeApplication()
    {
        if (Application.Current == null)
        {
            var app = new Application();
            var window = new Window();
            app.MainWindow = window;
        }

        Application.Current.MainWindow.Title = "Window Title";
    }
}

I was under the impression that the statement after await returns to the "original" context (where Application.Current.MainWindow was known).

Why does the async unit test throw an exception? Is the reason unit test specific or can the same exception be thrown in my WPF application too?

Bruno V
  • 1,721
  • 1
  • 14
  • 32
  • It won't happen in a single threaded WPF application with async/await code. However, as unit tests are ran over a thread pool, the default synchronization context of the Task, created by async/await won't be the Dispatcher that owns the visual elements. See this similar post https://stackoverflow.com/questions/39385956/async-does-not-continue-on-same-thread-in-unit-test – zaphod-ii Dec 18 '19 at 15:30
  • 1
    Just a dumb question, why do you need to cover the UI logic by unit tests? This is a scope of integration an UI tests, IMO – Pavel Anikhouski Dec 18 '19 at 15:45
  • I agree. The production code was not written with unit testing in mind. There are references to `Application.Current.MainWindow` (which can be removed) and to `Application.Current.Dispatcher` (which can be replaced by an interface). My main concern was that the code wouldn't return to the "original" context in the WPF application and this was the easiest way to provide an example. – Bruno V Dec 18 '19 at 16:05

1 Answers1

1

I was under the impression that the statement after await returns to the "original" context (where Application.Current.MainWindow was known).

It resumes on the captured SynchronizationContext provided that there is any context to capture by the time you call await.

In the context of an MSTest unit test, System.Threading.SynchronizationContext.Current returns null so there is no context to resume on once your Task has completed.

You could try to set the SynchronizationContext by calling SynchronizationContext.SetSynchronizationContext. A WPF application uses a System.Windows.Threading.DispatcherSynchronizationContext by default.

mm8
  • 163,881
  • 10
  • 57
  • 88
  • XUnit.NET has a synchronization context. – Paulo Morgado Dec 18 '19 at 15:44
  • 2
    @PauloMorgado: But MSTest hasn't. – mm8 Dec 18 '19 at 15:46
  • Sorry. I was too quick. A MSTest unit test hasn't but others might (and do) have. So, "In the context of a unit test, `System.Threading.SynchronizationContext.Current` returns `null`" is not stricktly right. Just a comment for someone looking for this that happens to be using XUnit.NET. – Paulo Morgado Dec 18 '19 at 15:49
  • 1
    @PauloMorgado: I edited the answer to include "in the context of an *MSTest* unit test" for clarification. – mm8 Dec 18 '19 at 15:50
  • @mm8: I have tried setting `SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());` at the end of my InitializeApplication method, but it made no difference. Is this the right approach? – Bruno V Dec 18 '19 at 15:54
  • 2
    @BrunoV: The right approach would be not to test any WPF framework code in your unit tests. You should focus on testing your code. A hint is to read up on the MVVM pattern and write unit test against your view models: https://stackoverflow.com/a/57008381/7252182 – mm8 Dec 18 '19 at 16:04