分享
 
 
 

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

王朝other·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

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

Author: Cpluser

Website:http://tdd.nease.net

Email:cpluser@hotmail.com

Blog:http://blog.csdn.net/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

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有