我在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代码,以便在这种情况下更容易进行单元测试。
有什么想法吗?
答案 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有助于缓解这一点,但可能有助于添加一些逻辑,并从任务中获取值。
答案 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);
}