单元测试async void事件处理程序

时间:2016-06-16 11:22:23

标签: c# winforms unit-testing events async-await

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

My View和Presenter如下(没有所有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!"
    }

我正在使用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);
}

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

我理解为什么会这样,并且设法通过在Thread.Sleep(500)之前添加Assert来解决这个问题,但这不太理想。我也可以更新我的视图来调用presenter.Save()方法,但我希望View尽可能与Presenter无关。

所以我想知道我可以改进单元测试,等待async View_SaveClicked完成或更改View / Presenter代码,以便在这种情况下更容易进行单元测试。

有什么想法吗?

3 个答案:

答案 0 :(得分:2)

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

您可以使用我的AsyncContext type

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

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

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

但是,在您自己的代码中avoid async void最好(正如我在关于异步最佳做法的MSDN文章中所描述的那样)。我有一篇专门介绍&#34; async event handlers&#34;。

的几种方法的博客文章

一种方法是用普通代表替换所有EventHandler<T>个事件,并通过await调用它:

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

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

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)));
}

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

另一种方法是使用&#34;延期&#34;扩展EventArgs。这也不是很好,但对于异步事件处理程序更为惯用。

答案 1 :(得分:0)

必须对正在运行的任务执行某些类型的工作,并且您需要使用某些东西来从任务返回值。

看起来像Thread.Sleep有助于缓解这一点,但可能有助于添加一些逻辑,并从任务中获取值。

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

答案 2 :(得分:-1)

为了完整起见,这里是使用Func而不是事件的工作代码:

public interface IExampleView
{
    Func<Object, EventArgs, Task> SaveClicked { get; set; }
    string Message { get; set; }
}

public partial class ExampleView : Form
{
    public Func<Object, EventArgs, Task> SaveClicked { get; set; }

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

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

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

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

        View.Message = "Saved!"
    }
}

测试:

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

    // Act
    View.SaveClicked(new object(), new EventArgs()).Wait();

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

[TestMethod]
public async Task WhenSaveButtonClicked_ThenSaveMessageShouldBeShown()
{
    // Arrange

    // Act
    await View.SaveClicked(new object(), new EventArgs());

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