显示标签为“practice”的博文。显示所有博文
显示标签为“practice”的博文。显示所有博文

2007年3月16日星期五

MVP实践之:单元测试

MVP模式的应用中很重要一点是“方便了测试”。MVP中的Model层不与界面发生任何关系,按照一般的测试方法进行测试即可。但是Presenter需要view,至少需要一个View的契约,也就是它多少需要对view有一些了解,那么到底如何在没有界面的情况下测试呢?这里借用上一篇中讲到的winform的例子,以测试SearchPictOfComp为目标来实践一下。这里假设Model层的代码都经过了测试。

我们编写一个测试用例,如下:
[Test]
public void SearchArtifactOfCompTest()
{
MockViewListDescPict view;
view=测试用的View;
view.ComponentId = "265f1e5d-2c75-4368-9a3a-5da5d82270c5";
ListDescPictPresenter presenter = new ListDescPictPresenter(view);
view.AttachPresenter(presenter);
view执行click button的动作;
Assert.IsTrue(((IList)view.descPictList).Count == 6);
}
先解释一下这个测试用例的意思。首先创建一个测试用的view,然后用它初始化presenter,当view执行一个点击button的动作之后,presenter会去响应这个操作,将取出的数据传给view的列表控件显示,如果成功则列表控件的数据源中应该有6条纪录。
现在运行这个测试用例肯定是失败的,因为标记为红色的代码都是没有实现的伪代码。为了让测试通过,我们尝试实现红色标记处所要求的功能。
  • view=测试用的View
    这里实际上是要求一个满足契约IViewListDescPict的view,我们虽然没有真正的view,但是我们可以编写一个模拟的view(mock view)——可以联想在模块化测试时编写桩(stub)模块的情形。MockViewListDescPict是一个模拟的view,它实现了IViewListDescPict接口。那么一个问题就解决了:
    view=new MockViewListDescPict();
  • view执行click button的动作
    上一篇中讲到presenter是采用观察者模式,它监听button的事件。既然只要事件,那我们只要模拟一个事件就行了,并不需要真的用一个button。搞清楚button的click是产生一个普通的event,那我们定义一个:
    public EventHandler searchArtsOfComp_Event;
    事件的触发很简单:searchArtsOfComp_Event.Invoke(null, null);
    我们把触发的代码放在一个方法中,以便在测试的时候模拟:
    public void MockClickSearchButton()
    {
    searchArtsOfComp_Event.Invoke(null, null);
    }
    接下来我们要让presenter注册对该事件的监听,以便在invoke触发事件的时候能被presenter捕捉到,我们把这一步放在AttachPresener中进行:
    public void AttachPresenter(ListDescPictPresenter presenter)
    {
    this.presenter = presenter;
    searchArtsOfComp_Event = presenter.SearchPictOfComp_EventHandler;
    }
    其中presenter.SearchPictOfComp_EventHandler是一个指向方法SearchPictOfComp(见应用篇)的委托。
    到此,第二个问题也解决了:
    view.MockClickSearchButton();
现在运行一下测试用例,pass了自然高兴,可是“(IList)view.descPictList”是怎么回事?其实这是为了测试读取的数据是不是正确地传给了view的列表控件,在解释测试用例的意思已经说过了。但是我们现在使用的view都是模拟(Mock)的,button事件也是假的,那么列表也同样可是假的了。我们并不关心列表控件是不是正确的显示了数据,那是控件自己的事情,我们只关心是不是传递了正确的数据,因此这里可以模拟一个接受数据的行为。控件的datasource是object的,那我们就在mock view的内部定义一个objcect的对象来获得对传递数据的引用:
public object descPictList;
public void SetPictListDataSource(IList pictList)
{
descPictList = pictList;
}
当我们模拟了button的click事件之后,presenter根据它的内部逻辑会去调用view的
SetPictListDataSource方法,将数据传递给我们设定的接收对象descPictList,现在我们只需要检查descPictList所获得的数据是否正确就行了。如果你愿意的话可以逐条检查descPictList中的项是否正确,但其实我们只需简单地检查项的个数就可以了,因为在presenter内部未对数据做修改,只要Model层能保证取出的数据无误,这里就不会有错。

总结一下对presenter的测试,一共有三点:
1、不需要真的view,可以使用类似Mock Object这样的技术模拟一个view,只要模拟的对象满足IView的契约即可。
2、如果要从view中获得用户的数据,比如要从textbox中取得用户输入的查询条件,又或者需要将处理的结果数据发回view显示,比如将一个list的集合发送到ListBox显示,这两类问题我们都可以使用对字段或者属性的存取操作来模拟。因为我们不关心显示,只要数据正确传送即可,将显示的任务交给view,可是使得presenter的功能独立于显示层。
3、对于像用户点击button或者选择chechbox这样的交互行为,我们不需要真的用控件来测试。.net允许我们手动触发事件,比如EventHandler.Invoke(),因此我们可以用手动触发事件的方式来模拟用户的交互行为。

2007年3月15日星期四

MVP实践之:Winform应用

上面的界面显示了程序最终运行的状态,主要完成两个功能:
  1. 输入一个构件的ID号,根据Id号到ftp服务器上查找该构件所有相关的图片,并且将这些图片的名称显示在一个ListBox中。
  2. 改变选中的图片(列表中选图片名)时将图片显示在右边的PictureBox中。
上面说的那两个功能,基本上可以说是这个程序的需求了,根据要求先画出界面来。注意这里画界面仅仅只是为了明确所需要的功能,既然是MVP模式的实践,我们就必须摆脱双击控件然后直接编写代码的习惯。将MVP的三要素从这个程序剥离出来,分析清楚它们各自的角色和职责,以及如何进行交互才是最首要的工作。
三要素:
View:是一个Form,包含了一些控件
Model:这里的功能就是访问ftp取得图片信息
Presenter:从界面获得构件id,根据id从model取得图片名称列表,然后传给界面显示;从界面获得图片名,根据图片名从model取得图片,将图片传给界面显示。

右边的图是实现该程序的类图,有两点需要先说明一下:
  1. Model在哪里?
    前面的文章“MVP vs MVC”中讲到过,MVP中的Model并非DomainModel,其中甚至包含了业务逻辑,就如在本程序中充当Model角色的是一个Service类,它负责从数据源取得数据并向外提供,需要的时候会先对数据做一定的处理。这里的数据源是一个ftp服务器。
  2. IView***是干什么的?
    使用MVP的一个很重要的原因是为了方便测试,将界面与数据和控制逻辑分离之后,可以在不依赖于具体的界面的情况下对View以下的逻辑层(V、P层)的代码进行测试。但是如果没有了具体的View,presenter与View的通信如何完成呢?引入“契约”可以解决这个问题。契约的基本思想是:一个对象它保证为它的使用者(调用它的对象)提供它所承诺的功能,而承诺的功能就是契约中所规定的功能。比如有这样一个规定:
    string SelectedPictName { get;}
    意思是所有的使用者都能够得到一个PictName,而所有遵守这个契约的对象都保证提供一个PictName。
    那么,本例中的IViewListDescPict就是这样一个契约,它规定了所有实现了这一接口的View都必须保证提供接口中所规定的功能(类实现接口,就必须实现接口中所有的功能)。这样presenter就不需要一个具体的View了,它只需要一个view的契约,通过调用契约中的功能来帮助自己完成任务。
以根据id查找构件所有的图片为例,看一下是如何工作的:

步骤1-4都是一些初始化工作,窗体作为视图负责初始化自己,然后创建一个它需要使用的presenter并且将自己与presenter邦定(实际上就是获得一个对presenter的引用)。
private void FormListDescPict_Load(object sender, EventArgs e)
{
presenter =new ListDescPictPresenter(this);
AttachPresenter(presenter);//this.presenter=presenter;
}
步骤5中view接受一个用户响应,但是它自己并不做处理而是在步骤6中将处理委托给presenter。
//register the presenter as a observer
btnSearch.Click +=presenter.SearchPictOfComp_EventHandler;
步骤6-11是一套控制逻辑,全部在presenter内部处理。presenter监视view中的按钮(观察者模式),当检测到用户点击事件之后,先从view取得componentid,从service取得数据,然后刷新view的数据显示。
private void SearchPictOfComp(Object sender,EventArgs e)
{
try
{
IList descPicts = pictService.GetPictsOfComp(view.ComponentId);
view.SetPictListDataSource(descPicts);
}
catch (Exception ex)
{
//View will show the error msg to the end user
view.Message = ex.Message;
}
}
我们可以看到view也就是这里的窗体form仅仅只是将用户的输入向下层传递,它甚至不需要知道自己什么时候该刷新,只是定义了刷新的方法,真正的刷新命令由presenter发出。view中没有逻辑处理,这样可以使得view尽量的简化。复杂的控制逻辑都在presenter层处理,因此这些控制也就变得可以复用了,意味着我们可以将这一套逻辑处理用于多个view,理想的情况下可以同时支持winform的和asp.net的程序。

最后要提一下异常处理问题。由于view没有处理逻辑,使得我们在view中没有捕获异常的机会,因此只能在presenter层捕捉了。那么如何将捕获的异常消息通知view以显示给用户呢?本例中是在IView这个契约里定义了一个Message属性,presenter将捕获的异常消息写入这个属性中,只要实现IView接口的view都可以获取这个消息,并进行处理。
public string Message
{
get
{
return msg;
}
set
{
if (!string.IsNullOrEmpty(value))
{
msg = value;
//you can use the msg in the way you like
MessageBox.Show(msg);
}
}
}
上面的方法虽然是对于错误处理提出来的,但是同样可以用于其他的用途,比如我希望返回一个操作成功的提示消息。