可测试的 WinForms 应用程序






4.97/5 (22投票s)
使 WinForms 可测试
引言
如今,由于 WPF 越来越受欢迎,WinForms 应用程序主要属于遗留代码。当一个团队决定全新桌面应用程序的开发堆栈时,他们主要投票支持 WPF。 另一方面,当需要升级与 WinForms 紧密结合的现有软件时,对 WinForms 应用程序存在需求,对更高性能存在需求等等。
不同的方法
关于可测试的 WinForms 应用程序,有很多观点。 有些人声称无法测试 WinForms 应用程序,因为用户事件和业务逻辑之间存在大量依赖关系。
标准的 Windows 窗体包含 *Designer.cs* 分部类和一个包含用户操作事件处理程序的分部类。 因此,如果我们遵循相同的原则并在事件处理程序中尝试实现应用程序逻辑,我们可以得出结论,为这种应用程序编写测试将非常困难。 但是,如果我们从一定距离看待这个问题,我们可以得出结论,每个具有用户界面的应用程序都可以表示为三个主要组件之间的交互:数据、用户界面和业务逻辑。
这是 MV* 模式的基本思想。 如果我们成功地分离这三个组件,那么我们的应用程序就可以很好地进行测试。 我使用 MVP 模式制作了一个简单的 WinForms 应用程序。 整个项目可以从提供的链接下载。
在本文中,我将只使用代码片段来给出代码样子的总体思路。
但首先,关于 MVP 模式的几句话
MVP 代表 Model-View-Presenter。 Model 是一个包含数据的组件。 它只是我们窗体的数据持有者。 View 代表用户界面。 它包含我们窗体的设计描述。 Presenter 是我们 WinForms 应用程序中完成大部分工作的组件。 它订阅视图事件,这些事件是用户与我们窗体交互的结果(单击按钮、文本更改、选择更改等)以及操作系统与我们窗体交互的结果(加载、显示、绘制等)。 Presenter 需要处理所有这些事件,并在处理后对视图执行适当的操作。
代码结构
让我们从我们的视图组件开始。
public interface IProductView
{
event EventHandler ViewLoad;
event EventHandler<ProductViewModel> AddNewProduct;
event EventHandler<ProductViewModel> ModifyProduct;
event EventHandler<int> DeleteProduct;
event EventHandler<int> ProductSelected;
void PopulateDataGridView(IList<Product> products);
void ClearInputControls();
void ShowMessage(string message);
}
public partial class Products : Form, IProductView
{
...
}
我们创建了一个接口 IProductView
,它定义了 Presenter 和 用户界面组件将如何交互的规则。
现在,让我们看看我们的 presenter 组件。
public class ProductPresenter
{
private IProductView view;
private IProductDataAccess dataAccesService;
public ProductPresenter(IProductView view, IProductDataAccess dataAccesService)
{
this.view = view;
this.dataAccesService = dataAccesService;
SubsribeToViewEvents();
}
private void SubsribeToViewEvents()
{
view.ViewLoad += View_Load;
view.AddNewProduct += View_AddNewProduct;
view.ProductSelected += View_ProductSelected;
view.ModifyProduct += View_ModifyProduct;
view.DeleteProduct += View_DeleteProduct;
}
...
}
我们的 presenter 在其构造函数中采用 IProductView
接口。 通过这种方式,我们的视图(窗体)可以很容易地被另一个实现相同接口的视图替换。 此外,在模拟方面,我们可以很容易地通过构造函数注入这种依赖关系。 另一个组件是 IProductDataAccess
,它代表数据库接口。
public interface IProductDataAccess
{
IList<Product> GetAllProducts();
Product GetProduct(int id);
bool AddProduct(Product product);
bool DeleteProduct(int productId);
bool EditProduct(int productId, Product product);
string ErrorMessage { get; }
}
一个测试用例示例
这个测试用例展示了我们如何轻松地在 Presenter 组件中模拟外部依赖项。
[Test]
public void ExpectToCallAddProductOnAppropriateEventReceived()
{
IProductView view = Substitute.For<IProductView>();
IProductDataAccess dataAccess = Substitute.For<IProductDataAccess>();
ProductPresenter presenter = new ProductPresenter(view, dataAccess);
ProductViewModel viewModel = new ProductViewModel()
{
NameText = "Test",
PriceText = "2"
};
view.AddNewProduct +=
Raise.Event<EventHandler<ProductViewModel>>(view, viewModel);
dataAccess.Received().AddProduct(Arg.Is<Product>
(x=>x.Price == 2 && x.Name == "Test"));
}
结论
这只是一个简单的例子,说明了一个从抽象开始的架构有多强大。 最好总是从更高层次的一些组件开始,然后,定义接口,你定义这些组件将如何相互交互的规则。
可以从提供的链接下载包含更多测试的整个项目。
欢迎提出任何意见和建议。