首 页 ┆ 源码下载 ┆ IT学院 ┆ 字体下载 ┆ 模板下载 ┆ 源码发布 ┆ 广告合作 ┆ 网站地图 ┆ 虚拟主机 ┆ 中文域名
► 设为首页
► 加入收藏
► 联系我们
源码下载 >> ASP源码 | PHP源码 | ASP.net源码 | JSP源码 | CGI源码 | VC/C++源码 | VB源码 | Delphi源码 | Flash源码
文章学院 >> 网络编程 | 网页设计 | 图形图象 | 数据库 | 服务器 | 网络媒体 | 网络安全 | 操作系统 | 办公软件 | 软件开发 | 黑客知识
字体下载 >> 精制字体 | 非英字体 | 艺术字体 | 著名字体 | 哥特式 | 简单字体 | 手写体 | 节假日 | 图案字体 | 精度像素 | 中文字体
模板下载 >> 企业门户 | 数码网络 | 休闲娱乐 | 影视音乐 | 旅游名胜 | 文化艺术 | 电子商务 | 个性展示 | 登陆导航 | Flash模板
►►您当前的位置:源码园 → IT学院 → 软件开发 → VC编程 → 文章内容

使用测试优先方法开发用户界面

作者:Cpluser  来源:网上收集  发布时间:2007-3-27 10:17:32

使用测试优先方法开发用户界面

作者:Cpluser

演示代码下载

关键字:测试优先 测试驱动开发 Mock Objects CppUnit

1、概述

  测试优先是测试驱动开发(Test-Driven Development, TDD)的核心思想,它要求在编写产品代码前先编写基于产品代码的测试代码。在测试驱动开发的单元测试中,对GUI应用实施自动测试应该是测试驱动开发的软肋之一。由于界面的操作是有由人来完成的,所以要想在GUI中完成单元自动测试是有一定难度的。Kent Beck在它的《测试驱动开发》中就曾提到过这个问题。
  本文将通过一个例子来讲解在测试驱动开发中如何针对GUI进行单元测试。这个例子是David Astels著的《测试驱动开发实用指南(影印版)》中一个关于影片列表管理的例子。该书中文版即将在国内出版。书中讨论并介绍了开发这个例子的多种方法。笔者将介绍其中的一种,并且为了方便使用C++的朋友的学习,书中的代码我用C++写了一遍,类名和变量名尽量和原书保持一致,以方便阅读该书的C++读者。在此也要感谢David Astels给我们带来如此精彩的一本书。
  本文叙述背景为:CppUnit1.9.0, Visual
C++ 6.0, Windows2000 pro。文中叙述有误之处,敬请批评指正。如果读者对CppUnit还没有一定的了解,可以先参考笔者的另一篇文章《CppUnit测试框架入门》。

2、需求分析

  对于这个影片管理的应用,我们主要实现增加、删除和显示影片列表的功能。基于这些需求,我们可以画一张GUI草图,如图1:


图1

  界面的控件主要有:一个显示所有影片的列表listbox控件,一个填写新的影片名的edit控件,一个增加button控件,一个删除button控件。由此,我们的开发目标就十分的明确了。

3、编写UI测试代码

  这部分的UI测试代码主要是测试各个控件是否正确生成并且是可见的,以及测试一些控件的label文字是否正确。
  我们从TestCase继承一个类TestWidgets用于测试窗口,并添加四个测试,分别测试listbox、edit、add button、delete button。

class TestWidgets : public CppUnit::TestCase{     CPPUNIT_TEST_SUITE(TestWidgets);     CPPUNIT_TEST(testList);     CPPUNIT_TEST(testField);     CPPUNIT_TEST(testAddButton);     CPPUNIT_TEST(testDeleteButton);     CPPUNIT_TEST_SUITE_END();public:     TestWidgets();     virtual ~TestWidgets();public:     virtual void setUp();     virtual void tearDown();      void testList();     void testField();     void testAddButton();     void testDeleteButton();private:     MovieListWindow* m_pWindow;};
其中,MovieListWindow是一个窗口类。我们来看看其中的一个测试,请看代码中的注释。
void TestWidgets::testAddButton(){       //得到btn指针       CButton* pAddButton = m_pWindow->GetAddButton();       //检查是否生成btn       CPPUNIT_ASSERT(pAddButton->m_hWnd);       //检查btn是否可见       CPPUNIT_ASSERT_EQUAL(TRUE, ::IsWindowVisible(pAddButton->m_hWnd));       CString strText;       pAddButton->GetWindowText(strText);       CString strExpect = "Add";       //检查btn的Label文字是否正确       CPPUNIT_ASSERT_EQUAL(strExpect, strText);}
  编译测试代码,编译器会给我们一些出错信息。这要求我们必须马上编写产品代码以让编译通过。首先第一个要实现的产品代码就是MovieListWindow窗口类。
class AFX_EXT_CLASS MovieListWindow : public CDialog{public:     MovieListWindow(CWnd* pParent = NULL); // standard constructor     CListBox* GetMovieListBox(){return &m_MovieListBox;};     CEdit* GetMovieField(){return &m_MovieField;};     CButton* GetAddButton(){return &m_AddBtn;};     CButton* GetDeleteButton(){return &m_DeleteBtn;};     void Init();     // Dialog Data     //{{AFX_DATA(MovieListWindow)     enum { IDD = IDD_MOVIELISTDLG };     CButton m_AddBtn;     CButton m_DeleteBtn;     CEdit m_MovieField;     CListBox m_MovieListBox;     //}}AFX_DATA     // Overrides     // ClassWizard generated virtual function overrides     //{{AFX_VIRTUAL(MovieListWindow)  protected:     virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support     //}}AFX_VIRTUAL     // Implementation  protected:    // Generated message map functions    //{{AFX_MSG(MovieListWindow)    //}}AFX_MSG  DECLARE_MESSAGE_MAP()};
  在MovieListWindow窗口类中我们实现了需要的控件以及针对这些控件的一些方法,如GetMovieListBox()等,本文在此不做详述。编译测试代码和产品代码,检查是否通过。如未通过则继续检查产品代码以使编译和测试通过。

4、编写控件行为测试代码

  接下来应该是编写点击add button和delete button的测试代码了。同样,我们从TestCase继承出TestOperation:
class TestOperation : public CppUnit::TestCase{     CPPUNIT_TEST_SUITE(TestOperation);     CPPUNIT_TEST(testMovieList);     CPPUNIT_TEST(testAdd);     CPPUNIT_TEST(testDelete);     CPPUNIT_TEST_SUITE_END();public:     void testMovieList();     void testAdd();     void testDelete();public:     void setUp();     void tearDown();     TestOperation();     virtual ~TestOperation();private:     static CString LOST_IN_SPACE;     CStringArray m_MovieNames;     MovieListWindow* m_pWindow;     MovieListEditor* m_pEditor;};
  你会发现,在TestOperation类中出现了一个成员变量MovieListEditor* m_pEditor。类MovieListEditor是一个用来保存影片数据以及对影片数据进行增加 ,删除操作的管理类。后面我们会给出它的实现。看看setUp()做了什么:
void TestOperation::setUp(){     //创建一个MovieListEditor实例     m_pEditor = new MovieListEditor();     m_MovieNames.RemoveAll();     //将MovieListEditor中的影片列表拷贝到m_MovieNames,为后面测试作准备     for(int n=0; n<m_pEditor->GetMovies()->GetSize(); n++)     {        m_MovieNames.Add(m_pEditor->GetMovies()->GetAt(n));     }}
我们来看看添加影片的测试,请看代码注释:
void TestOperation::testAdd(){     //拷贝一份movie list     CStringArray MovieNamesWithAddition;     for(int n=0; n<m_MovieNames.GetSize(); n++)     {        MovieNamesWithAddition.Add(m_MovieNames.GetAt(n));     }     MovieNamesWithAddition.Add(LOST_IN_SPACE);     //生成窗口     MovieListWindow *pWindow = new MovieListWindow(m_pEditor);     pWindow->Init();     //填写新的影片的名称     CEdit* pEdit = pWindow->GetMovieField();     pEdit->SetWindowText(LOST_IN_SPACE);     //点击add btn      CButton* pBtn = pWindow->GetAddButton();     ::SendMessage(pBtn->m_hWnd, BM_CLICK, 0, 0);     //检查列表控件中是否已加入新的影片     CListBox* pListBox = pWindow->GetMovieListBox();     CPPUNIT_ASSERT_EQUAL(MovieNamesWithAddition.GetSize(), pListBox->GetCount());     //检查列表控件中影片名是否正确     CString strNewMovieName;     pListBox->GetText(pListBox->GetCount()-1, strNewMovieName);     CPPUNIT_ASSERT_EQUAL(LOST_IN_SPACE, strNewMovieName);     //销毁窗口     pWindow->DestroyWindow();     delete pWindow;     pWindow = NULL;}
编译后会有出错信息,主要的错误有:
a)、我们把m_pEditor保存在MovieListWindow中了,这需要我们修改原来的MovieListWindow的构造函数。
b)、没有MovieListEditor类。

MovieListEditor的实现如下:
class AFX_EXT_CLASS MovieListEditor {public:     MovieListEditor();     virtual ~MovieListEditor();public:     virtual CStringArray* GetMovies(){return &m_arMovieList;};     virtual void Add(CString strMovie){m_arMovieList.Add(strMovie);};     virtual void Delete(int nIndex){m_arMovieList.RemoveAt(nIndex);};private:     CStringArray m_arMovieList;};
再次编译,已经通过.运行测试,发现在:
CPPUNIT_ASSERT_EQUAL(MovieNamesWithAddition.GetSize(), pListBox->GetCount());
测试通不过。检查后知道原因是,我们在测试代码里:
::SendMessage(pBtn->m_hWnd, BM_CLICK, 0, 0);
给add button发送了点击按钮的消息,但是在MovieListWindow 窗口中我们没有加入消息的响应函数,因此测试没有通过。赶紧添加消息响应函数。
void MovieListWindow::OnClickAddButton() {     UpdateData();     CString strNewMovieName;     m_MovieField.GetWindowText(strNewMovieName);     if("" != strNewMovieName)     {        m_pEditor->Add(strNewMovieName);        m_MovieListBox.AddString(strNewMovieName);     }}
编译、测试、通过。

5、Mock Objects

  在删除操作的单元测试中,我们遇到的一个问题是,影片列表的数据应该是保存在一个文本文件或者数据库当中的,如果我们编写的测试依赖于这些实际的文件或数据库,那么我们的测试就会受制于这些外部的资源。一旦文件或者数据库里的数据发生变化,必然会波及到我们的测试代码,从而产生错误的测试信息。前面的MovieListEditor中我们没有加入一些初始化的数据,在测试删除操作时会遇到一些问题 。
  这里,我们引入Mock Objects。Mock Objects用来模拟外部复杂的资源(如数据库,网络连接等),使UI可以测试那些依赖于这些复杂外界资源的模块。例如在测试一个跟数据库有关系的模块时,我们并不一定要建立一个真实的数据库连接,而只需建立一个Mock Objects就可以了。测试所需的数据都存在于这个Mock Objects。可以说,Mock Objects为我们提供了一个轻量级的、可控制的、高效的模型。
  在本例中,影片的增加、删除都会跟文件或数据库操作发生关系。这时我们就可以利用Mock Objects来隔离测试代码与文件或数据库。使用Mock Objects一般有以下几个步骤:
a)、定义一个外部资源的接口.(这个接口一般是可以在重构过程中提炼出来的)。
b)、定义一个Mock Objects,从外部资源的接口继承下来,实现外部资源的接口。
c)、创建一个Mock Objects,并设置它的内部期望值。
d)、把创建的这个Mock Objects传递给需要测试的模块进行操作。
e)、操作完毕后将Mock Objects内部的状态与期待状态比较。现在我们就根据这个步骤来实现本例子中的Mock Objects.通过对前面的代码进行重构,我们可以提炼出一个接口MovieListEditor:
class AFX_EXT_CLASS MovieListEditor {public:    MovieListEditor();    virtual ~MovieListEditor();public:    virtual CStringArray* GetMovies()=0;    virtual void Add(CString strMovie)=0;    virtual void Delete(int nIndex)=0;};
  请注意它和前面我们定义的MovieListEditor的不同。接下来,我们应该定义一个Mock Objects,当然它是从MovieListEditor继承下来的:
class mockEditor : public MovieListEditor{public:     mockEditor();     virtual ~mockEditor();public:     virtual CStringArray* GetMovies(){return &m_arMovieList;};     virtual void Add(CString strMovie){m_arMovieList.Add(strMovie);};     virtual void Delete(int nIndex){m_arMovieList.RemoveAt(nIndex);};private:     CStringArray m_arMovieList;};
然后给这个Mock Objects设置初识值,我们选择在它的构造函数里进行。
mockEditor::mockEditor(){     m_arMovieList.Add("Star Wars");     m_arMovieList.Add("Star Trek");     m_arMovieList.Add("Stargate");}
  我们添加了三个影片用于测试。接着,应该把这个MockObjects的一个实例传递给需要测试的模块。这里就是我们要测试的UI(MovieListWindow)。
m_pEditor = new mockEditor();   MovieListWindow *pWindow = new MovieListWindow(m_pEditor);
最后我们来看看经过修改后的新的测试添加影片的方法:
void TestOperation::testAdd(){     //拷贝一份movie list      CStringArray MovieNamesWithAddition;     for(int n=0; n<m_MovieNames.GetSize(); n++)     {        MovieNamesWithAddition.Add(m_MovieNames.GetAt(n));     }     MovieNamesWithAddition.Add(LOST_IN_SPACE);     //生成窗口     MovieListWindow *pWindow = new MovieListWindow(m_pEditor);     pWindow->Init();     //填写新的影片的名称     CEdit* pEdit = pWindow->GetMovieField();     pEdit->SetWindowText(LOST_IN_SPACE);     //点击add btn      CButton* pBtn = pWindow->GetAddButton();     ::SendMessage(pBtn->m_hWnd, BM_CLICK, 0, 0);     //检查列表控件中是否已加入新的影片     CListBox* pListBox = pWindow->GetMovieListBox();     CPPUNIT_ASSERT_EQUAL(MovieNamesWithAddition.GetSize(), pListBox->GetCount());     //将Mock Objects的内部数据和期望值进行比较     CPPUNIT_ASSERT_EQUAL(MovieNamesWithAddition.GetSize(),      m_pEditor->GetMovies()->GetSize());     //检查列表控件中影片名是否正确     CString strNewMovieName;     pListBox->GetText(pListBox->GetCount()-1, strNewMovieName);     CPPUNIT_ASSERT_EQUAL(LOST_IN_SPACE, strNewMovieName);     //将Mock Objects的内部数据和期望值进行比较     int nIndex = m_pEditor->GetMovies()->GetSize();     CPPUNIT_ASSERT_EQUAL(LOST_IN_SPACE, m_pEditor->GetMovies()->GetAt(nIndex-1));     //销毁窗口     pWindow->DestroyWindow();     delete pWindow;     pWindow = NULL;}
  请注意,这里测试的数据都是mockEditor里的,而且在UI进行添加操作后,还将mockEditor内部的状态与期待状态做了比较。
CPPUNIT_ASSERT_EQUAL(MovieNamesWithAddition.GetSize(), m_pEditor->GetMovies()->GetSize());   CPPUNIT_ASSERT_EQUAL(LOST_IN_SPACE, m_pEditor->GetMovies()->GetAt(nIndex-1));
  其他删除操作的测试跟添加类似,在此不做详述。至此,我们就完成了这个GUI应用程序的开发。所有的测试如图2所示:


图2

6、
源码说明

  本文附带的代码包括三个Project,分别是Movie、GuiTestFirst、AppMovieList.Movie是产品代码.GuiTestFirst是测试代码 。AppMovieList是使用Movie输出的产品代码而写的应用程序,它从MovieListEditor继承出一个新的影片管理类MyEditor。它主要是演示如何使用我们提炼出来的MovieListEditor接口 。例如你可以实现CXmlMovieListEditor,CAccessMovieListEditor等等。进入GuiTestFirst打开所有这些工程。AppMovieList运行如图3所示 :


图3

7、总结

a)、对GUI应用实施测试优先开发方法,这在测试驱动开发中并不是必须的,可根据开发的实际情况来选择。
b)、我们通过引入Mock Objects,我们使测试代码和外部复杂的资源隔离开来,同时也使我们能够从中既有代码中提炼出清晰的接口,使代码整洁可用。

8、参考资料
  • 《测试驱动开发实用指南(影印版)》David Astels
  • 《测试驱动开发(中文版)》Kent Beck
  • 《Endo-Testing: Unit Testing with Mock Objects》Tim Mackinnon, Steve Freeman, Philip Craig

9、作者联系方式

  • Website:http://tdd.nease.net
  • Email:cpluser@hotmail.com
  • Blog:http://blog.csdn.net/cpluser/

[] [返回上一页] [打 印]
  • 上一篇文章:解说Win32的窗口子类化
  • 下一篇文章:利用非模窗口生成MDI介面

  • 相关文章:
  • 开发基于Java的图形用户界面
  • [组图]使用测试优先方法开发用户界面
  • 网站设计和图形用户界面(GUI)设计的不同
关于本站 - 网站帮助 - 广告合作 - 下载声明 - 友情连接 - 网站地图 - 源码发布
Copyright © 2003-2009 Ymyasp.Com. All Rights Reserved .
备案序号:粤ICP备07029071号