Unit testing async void event handler
Asked Answered
S

3

7

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?

Slumlord answered 16/6, 2016 at 11:22 Comment(1)
Related: Await an async void method call for unit testing.Mungo
A
4

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.

Amphibolite answered 16/6, 2016 at 13:36 Comment(5)
AsyncContext looks promising but requires .NET 4.6 and we are using 4.5 :(Slumlord
With the public Func<Object, EventArgs, Task> SaveClicked suggestion, what would the presenter and unit test code look like?Slumlord
I have figured it out. Controller: View.SaveClicked = View_SaveClicked; private async Task View_SaveClicked(object sender, EventArgs e) { } Unit test: View.SaveClicked(new object(), new EventArgs()).Wait();Slumlord
@Langers: No, don't use Wait; use await. AsyncContext is available for 4.5 if you use the AsyncEx library.Amphibolite
I am not sure about the AsyncContext.Run as a solution to this problem. The first (minor) issue is that it blocks the current thread, and the second (more serious) is that it alters the behavior of the async void method. For example the method might contain sync-over-async code that is harmless in a context-free environment, but it causes a deadlock in the single-threaded synchronization context used by the AsyncContext.Run. I have posted here an alternative approach that might be better.Mungo
B
0

There must be a some type work being done of the running task, and you need to use something to return a value from the task.

Seems like the Thread.Sleep helps mitigate that, though, might help to add some logic, and get a value from the task.

From: https://msdn.microsoft.com/en-us/library/mt674882.aspx

Blazonry answered 16/6, 2016 at 16:21 Comment(0)
H
0

This works for me:

View:

await OnGetAll?.Invoke(this, <Event param>);

Presenter:

_view.OnGetAll += MyFunction;

Interfaz View

event Func<object, EventArg, Task> OnGetAll;

I replace this:

event EventHandler? OnGetAll
Hexa answered 7/2 at 9:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.