单元测试异步void事件处理程序

Lan*_*ers 5 c# events unit-testing winforms async-await

我已经在c#winforms中实现了MVP(MVC)模式。

我的视图和演示者如下(没有所有的MVP胶水):

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!"
    }
Run Code Online (Sandbox Code Playgroud)

我正在使用MSTest进行单元测试,并使用NSubstitute进行模拟。我想模拟视图中的按钮单击以测试控制器的View_SaveClicked代码,如下所示:

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

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

    // Assert
    Assert.AreEqual("Saved!", View.Message);
}
Run Code Online (Sandbox Code Playgroud)

我可以View.SaveClicked使用NSubstitute的成功提出Raise.EventWith。但是,问题在于,Assert在Presenter有时间保存消息之前,代码立即前进到Presenter,并且Assert失败。

我了解为什么会发生这种情况,并设法通过在Thread.Sleep(500)之前添加来解决此问题Assert,但这并不理想。我也可以更新视图以改为调用presenter.Save()方法,但是我希望视图尽可能与Presenter无关。

因此,我想知道我可以改善单元测试,以等待async View_SaveClicked完成或更改View / Presenter代码以在这种情况下更轻松地进行单元测试。

有任何想法吗?

Ste*_*ary 4

由于您只关心单元测试,因此您可以使用自定义SynchronizationContext,它允许您检测async void方法的完成情况。

您可以使用我的AsyncContext类型来实现此目的:

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

  AsyncContext.Run(() =>
  {
    // Act
    View.SaveClicked += Raise.EventWith(new object(), new EventArgs());
  });

  // Assert
  Assert.AreEqual("Saved!", View.Message);
}
Run Code Online (Sandbox Code Playgroud)

但是,最好在您自己的代码中避免使用async void(正如我在有关异步最佳实践的 MSDN 文章中所描述的那样)。我有一篇博文专门介绍了“异步事件处理程序”的几种方法。

一种方法是将所有EventHandler<T>事件替换为普通委托,并通过以下方式调用它await

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

不过,如果你想要一个真实的事件,这就不那么漂亮了:

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)));
}
Run Code Online (Sandbox Code Playgroud)

使用这种方法,任何同步事件处理程序都需要Task.CompletedTask在处理程序末尾返回。

EventArgs另一种方法是通过“延期”来延长期限。这也不太漂亮,但对于异步事件处理程序来说更惯用。