Search code examples
c#winformsunit-testingeventsasync-await

Unit testing async void event handler


I have implemented the MVP (MVC) pattern in c# winforms.

My View and Presenter are as follows (without all the MVP glue):

public interface IExampleView
{
    event EventHandler<EventArgs> SaveClicked;
    string Message {get; set; }
}

public partial class ExampleView : Form
{
    public event EventHandler<EventArgs> SaveClicked;

    string Message { 
        get { return txtMessage.Text; } 
        set { txtMessage.Text = value; } 
    }

    private void btnSave_Click(object sender, EventArgs e)
    {
        if (SaveClicked != null) SaveClicked.Invoke(sender, e);
    }
}

public class ExamplePresenter
{
    public void OnLoad()
    {
        View.SaveClicked += View_SaveClicked;
    }

    private async void View_SaveClicked(object sender, EventArgs e)
    {
        await Task.Run(() => 
        {
            // Do save
        });

        View.Message = "Saved!"
    }

I am using MSTest for unit testing, along with NSubstitute for mocking. I want to simulate a button click in the view to test the controller's View_SaveClicked code as have the following:

[TestMethod]
public void WhenSaveButtonClicked_ThenSaveMessageShouldBeShown()
{
    // Arrange

    // Act
    View.SaveClicked += Raise.EventWith(new object(), new EventArgs());

    // Assert
    Assert.AreEqual("Saved!", View.Message);
}

I am able to raise the View.SaveClicked successfully using NSubstitute's Raise.EventWith. However, the problem is that code immediately proceeds to the Assert before the Presenter has had time to save the message and the Assert fails.

I understand why this is happening and have managed to get around it by adding a Thread.Sleep(500) before the Assert, but this is less than ideal. I could also update my view to call a presenter.Save() method instead, but I would like the View to be Presenter agnostic as much as possible.

So would like to know I can improve the unit test to either wait for the async View_SaveClicked to finish or change the View/Presenter code to allow them to be unit tested easier in this situation.

Any ideas?


Solution

  • Since you are just concerned about unit testing, then you can use a custom SynchronizationContext, which allows you to detect the completion of async void methods.

    You can use my AsyncContext type for this:

    [TestMethod]
    public void WhenSaveButtonClicked_ThenSaveMessageShouldBeShown()
    {
      // Arrange
    
      AsyncContext.Run(() =>
      {
        // Act
        View.SaveClicked += Raise.EventWith(new object(), new EventArgs());
      });
    
      // Assert
      Assert.AreEqual("Saved!", View.Message);
    }
    

    However, it's best to avoid async void in your own code (as I describe in an MSDN article on async best practices). I have a blog post specifically about a few approaches on "async event handlers".

    One approach is to replace all EventHandler<T> events with plain delegates, and call it via await:

    public Func<Object, EventArgs, Task> SaveClicked;
    private void btnSave_Click(object sender, EventArgs e)
    {
      if (SaveClicked != null) await SaveClicked(sender, e);
    }
    

    This is less pretty if you want a real event, though:

    public delegate Task AsyncEventHandler<T>(object sender, T e);
    public event AsyncEventHandler<EventArgs> SaveClicked;
    private void btnSave_Click(object sender, EventArgs e)
    {
      if (SaveClicked != null)
        await Task.WhenAll(
          SaveClicked.GetInvocationList().Cast<AsyncEventHandler<T>>
              .Select(x => x(sender, e)));
    }
    

    With this approach, any synchronous event handlers would need to return Task.CompletedTask at the end of the handler.

    Another approach is to extend the EventArgs with a "deferral". This is also not pretty, but is more idiomatic for asynchronous event handlers.