【问题标题】:Unit testing async void event handler单元测试异步 void 事件处理程序
【发布时间】:2016-06-16 11:22:23
【问题描述】:

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

我的 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 的 Raise.EventWith 成功提升 View.SaveClicked。但是,问题是代码在 Presenter 有时间保存消息之前立即转到 Assert 并且 Assert 失败。

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

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

有什么想法吗?

【问题讨论】:

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


    【解决方案1】:

    既然你只关心单元测试,那么你可以使用自定义的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 文章中关于异步最佳实践的描述)。我有一篇关于“async event handlers”的博客文章专门介绍了一些方法。

    一种方法是用普通委托替换所有EventHandler&lt;T&gt; 事件,并通过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

    另一种方法是使用“延迟”扩展EventArgs。这也不漂亮,但对于异步事件处理程序来说更惯用。

    【讨论】:

    • AsyncContext 看起来很有希望,但需要 .NET 4.6,我们正在使用 4.5 :(
    • 使用 public Func SaveClicked 建议,演示者和单元测试代码会是什么样子?
    • 我已经想通了。控制器:View.SaveClicked = View_SaveClicked;私有异步任务 View_SaveClicked(object sender, EventArgs e) { } 单元测试:View.SaveClicked(new object(), new EventArgs()).Wait();
    • @Langers:不,不要使用Wait;使用awaitAsyncContext is available for 4.5 if you use the AsyncEx library.
    【解决方案2】:

    运行中的任务必须完成某种类型的工作,并且您需要使用某些东西从任务中返回值。

    似乎 Thread.Sleep 有助于缓解这种情况,但可能有助于添加一些逻辑,并从任务中获取值。

    发件人:https://msdn.microsoft.com/en-us/library/mt674882.aspx

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-23
      • 1970-01-01
      • 2018-02-13
      • 1970-01-01
      • 1970-01-01
      • 2013-01-06
      相关资源
      最近更新 更多