Unit testing a class used in multi-threaded WPF application

644 Views Asked by At

I have written a C# class that sits in between the UI of a WPF Application and an Async Messaging System. I am now writing Unit Tests for this class and running into problems with the dispatcher. The following method belongs to the class I am testing and it creates a subscription handler. So I am calling this Set_Listing_Records_ResponseHandler method from the Unit Test - Test Method.

public async Task<bool> Set_Listing_Records_ResponseHandler(
    string responseChannelSuffix, 
    Action<List<AIDataSetListItem>> successHandler, 
    Action<Exception> errorHandler)
{
    // Subscribe to Query Response Channel and Wire up Handler for Query Response
    await this.ConnectAsync();
    return await this.SubscribeTo_QueryResponseChannelAsync(responseChannelSuffix, new FayeMessageHandler(delegate (FayeClient client, FayeMessage message) {
        Application.Current.Dispatcher.BeginInvoke(new Action(() =>
        {
            try
            {
                ...
            }
            catch (Exception e)
            {
                ...
            }
        }));
    }));
}

Execution flow comes back to the Application.Current.Dispatcher.... line but then throws the error:

Object reference not set to an instance of an object.

When I debug I can see that Application.Current is null.

I have done some searching around and found a number of examples of using a dispatcher in the Unit Test - Test Method, and I have tried some of these and they prevent the error, but the code in the dispatcher is never run.

I have not been able to find any examples where there is a Dispatcher used in the method that the Test Method is calling.

I am working in .NET 4.5.2 on a Windows 10 machine.

Any assistance would be greatly appreciated.

Thanks for your time.

2

There are 2 best solutions below

0
user2109254 On BEST ANSWER

Thanks to all who responded, your feedback was much appreciated.

Here is what I ended up doing:

I created a private variable in my UICommsManager class:

private SynchronizationContext _MessageHandlerContext = null;

and initialized this in the constructor of the UICommsManager. I then updated my message handlers to use the new _MessageHandlerContext instead of the Dispatcher:

public async Task<bool> Set_Listing_Records_ResponseHandler(string responseChannelSuffix, Action<List<AIDataSetListItem>> successHandler, Action<Exception> errorHandler)
{
    // Subscribe to Query Response Channel and Wire up Handler for Query Response
    await this.ConnectAsync();
    return await this.SubscribeTo_QueryResponseChannelAsync(responseChannelSuffix, new FayeMessageHandler(delegate (FayeClient client, FayeMessage message) {
        _MessageHandlerContext.Post(delegate {
            try
            {
                ...
            }
            catch (Exception e)
            {
                ...
            }
        }, null);
    }));
}

When used from the UI the UICommsManager class gets SynchronizationContext.Current passed into the constructor.

I updated my Unit Test to add the following private variable:

private SynchronizationContext _Context = null;

And the following method which initializes it:

#region Test Class Initialize

[TestInitialize]
public void TestInit()
{
    SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
    _Context = SynchronizationContext.Current;
}

#endregion

And then the UICommsManager gets the _Context variable passed into it's constructor from the Test Method.

Now it works in both scenarios, when called by WPF, and when called by Unit Test.

0
Stephen Cleary On

Proper modern code will never make use of the Dispatcher. It ties you to a specific UI and makes it hard to unit test (as you discovered).

The best approach is to implicitly capture the context and resume on it, using await. For example, if you know that Set_Listing_Records_ResponseHandler will always be called from the UI thread, then you can do something like this:

public async Task<bool> Set_Listing_Records_ResponseHandler(
    string responseChannelSuffix, 
    Action<List<AIDataSetListItem>> successHandler, 
    Action<Exception> errorHandler)
{
  // Subscribe to Query Response Channel and Wire up Handler for Query Response
  await this.ConnectAsync();
  var tcs = new TaskCompletionSource<FayeMessage>();
  await this.SubscribeTo_QueryResponseChannelAsync(responseChannelSuffix, new FayeMessageHandler(
      (client, message) => tcs.TrySetResult(message)));
  var message = await tcs.Task;
  // We are already back on the UI thread here; no need for Dispatcher.
  try
  {
    ...
  }
  catch (Exception e)
  {
    ...
  }
}

However, if you're dealing with an event kind of system where notifications can arrive unexpectedly, then you can't always use the implicit capture of await. In this case, the next best approach is to capture the current SynchronizationContext at some point (e.g., object construction) and queue your work to that instead of to the dispatcher directly. E.g.,

private readonly SynchronizationContext _context;
public Constructor() // Called from UI thread
{
  _context = SynchronizationContext.Current;
}
public async Task<bool> Set_Listing_Records_ResponseHandler(
    string responseChannelSuffix, 
    Action<List<AIDataSetListItem>> successHandler, 
    Action<Exception> errorHandler)
{
  // Subscribe to Query Response Channel and Wire up Handler for Query Response
  await this.ConnectAsync();
  return await this.SubscribeTo_QueryResponseChannelAsync(responseChannelSuffix, new FayeMessageHandler(delegate (FayeClient client, FayeMessage message) {
    _context.Post(new SendOrPostCallback(() =>
    {
        try
        {
            ...
        }
        catch (Exception e)
        {
            ...
        }
    }, null));
  }));
}

However, if you feel you must use dispatcher (or just don't want to clean up the code right now), you can use the WpfContext which provides a Dispatcher implementation for unit testing. Note that you still can't use Application.Current.Dispatcher (unless you use MSFakes) - you'd have to capture the dispatcher at some point instead.