[英]Unit testing async void event handler
我在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!"
}
我正在使用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);
}
我能夠提高View.SaveClicked
成功地利用NSubstitute的Raise.EventWith
。 然而,問題是,代碼將立即進行Assert
演示有時間來保存該消息之前和Assert
失敗。
我理解為什么會發生這種情況並且設法通過在Assert
之前添加Thread.Sleep(500)
來解決它,但這不太理想。 我也可以更新我的視圖來調用presenter.Save()
方法,但我希望View盡可能地與Presenter無關。
所以我想知道我可以改進單元測試,以等待async View_SaveClicked
完成或更改View / Presenter代碼,以便在這種情況下更容易進行單元測試。
有任何想法嗎?
由於您只關心單元測試,因此您可以使用自定義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);
}
但是,最好避免在自己的代碼中出現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);
}
如果你想要一個真實的事件 ,這不太漂亮,但是:
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
。 這也不是很好,但對於異步事件處理程序來說更為慣用。
必須對正在運行的任務執行某種類型的工作,並且您需要使用某些東西從任務中返回值。
看起來像Thread.Sleep有助於緩解這種情況,但可能有助於添加一些邏輯,並從任務中獲取值。
來自: https : //msdn.microsoft.com/en-us/library/mt674882.aspx
為了完整起見,這里是使用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);
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.