全面介绍单元测试
上一篇 / 下一篇 2008-08-20 00:24:23 / 天气: 冷 / 心情: 伤感 / 精华(1) / 置顶(1) / 个人分类:学习
一 单元测试概述51Testing软件测试网NT;G2Q7\
工厂在组装一台电视机之前,会对每个元件都进行测试,这,就是单元测试。51Testing软件测试网+\6U:Foy;L
其实我们每天都在做单元测试。你写了一个函数,除了极简单的外,总是要执行一下,看看功能是否正常,有时还要想办法输出些数据,如弹出信息窗口什么的,这,也是单元测试,老纳把这种单元测试称为临时单元测试。只进行了临时单元测试的软件,针对代码的测试很不完整,代码覆盖率要超过70%都很困难,未覆盖的代码可能遗留大量的细小的错误,这些错误还会互相影响,当BUG暴露出来的时候难于调试,大幅度提高后期测试和维护成本,也降低了开发商的竞争力。可以说,进行充分的单元测试,是提高软件质量,降低开发成本的必由之路。51Testing软件测试网,TE G3S*c
对于程序员来说,如果养成了对自己写的代码进行单元测试的习惯,不但可以写出高质量的代码,而且还能提高编程水平。
#vx!a~2Fj k0 要进行充分的单元测试,应专门编写测试代码,并与产品代码隔离。老纳认为,比较简单的办法是为产品工程建立对应的测试工程,为每个类建立对应的测试类,为每个函数(很简单的除外)建立测试函数。首先就几个概念谈谈老纳的看法。51Testing软件测试网h x|*gBB(zK/A
一般认为,在结构化程序时代,单元测试所说的单元是指函数,在当今的面向对象时代,单元测试所说的单元是指类。以老纳的实践来看,以类作为测试单位,复杂度高,可操作性较差,因此仍然主张以函数作为单元测试的测试单位,但可以用一个测试类来组织某个类的所有测试函数。单元测试不应过分强调面向对象,因为局部代码依然是结构化的。单元测试的工作量较大,简单实用高效才是硬道理。
y:T(X3O}+t y}T0 有一种看法是,只测试类的接口(公有函数),不测试其他函数,从面向对象角度来看,确实有其道理,但是,测试的目的是找错并最终排错,因此,只要是包含错误的可能性较大的函数都要测试,跟函数是否私有没有关系。对于C++来说,可以用一种简单的方法区隔需测试的函数:简单的函数如数据读写函数的实现在头文件中编写(inline函数),所有在源文件编写实现的函数都要进行测试(构造函数和析构函数除外)。
l A*dz4L0 什么时候测试?单元测试越早越好,早到什么程度?XP开发理论讲究TDD,即测试驱动开发,先编写测试代码,再进行开发。在实际的工作中,可以不必过分强调先什么后什么,重要的是高效和感觉舒适。从老纳的经验来看,先编写产品函数的框架,然后编写测试函数,针对产品函数的功能编写测试用例,然后编写产品函数的代码,每写一个功能点都运行测试,随时补充测试用例。所谓先编写产品函数的框架,是指先编写函数空的实现,有返回值的随便返回一个值,编译通过后再编写测试代码,这时,函数名、参数表、返回类型都应该确定下来了,所编写的测试代码以后需修改的可能性比较小。
!kYamK'iz]0 由谁测试?单元测试与其他测试不同,单元测试可看作是编码工作的一部分,应该由程序员完成,也就是说,经过了单元测试的代码才是已完成的代码,提交产品代码时也要同时提交测试代码。测试部门可以作一定程度的审核。51Testing软件测试网~M7Oy]?
关于桩代码,老纳认为,单元测试应避免编写桩代码。桩代码就是用来代替某些代码的代码,例如,产品函数或测试函数调用了一个未编写的函数,可以编写桩函数来代替该被调用的函数,桩代码也用于实现测试隔离。采用由底向上的方式进行开发,底层的代码先开发并先测试,可以避免编写桩代码,这样做的好处有:减少了工作量;测试上层函数时,也是对下层函数的间接测试;当下层函数修改时,通过回归测试可以确认修改是否导致上层函数产生错误。
S+{!wM"}P;o5mz&cNW0
9e?$p.{"}@&c r}0二 测试代码编写
)BHSM_y0 多数讲述单元测试的文章都是以Java为例,本文以C++为例,后半部分所介绍的单元测试工具也只介绍C++单元测试工具。下面的示例代码的开发环境是VC6.0。
K+I:eJ'f0
C \}+^$C2d0产品类:51Testing软件测试网4]Rp+kV$IA
class CMyClass
u-E P*nr0L Di^;`I0{51Testing软件测试网&c!~d~5N'P
public:51Testing软件测试网 Z"SU%~{)X~!w
int Add(int i, int j);51Testing软件测试网,sY^/JK
CMyClass();
c7~ZOE,~ ue)s0 virtual ~CMyClass();
Ye4Y%{~&r1Z@m051Testing软件测试网3~ aFz D4i-sw
private:
0r-PDT[&Co A0 int mAge; //年龄
#q$t5EMu5?2m0 CString mPhase; //年龄阶段,如"少年","青年"51Testing软件测试网%[5kDQlF|
};51Testing软件测试网_+V.bS5RK
'bZ s;] i5nhB0建立对应的测试类CMyClassTester,为了节约编幅,只列出源文件的代码:51Testing软件测试网c$j j}ZU@
void CMyClassTester::CaseBegin()
.y;j7L\7Q3hBv0{51Testing软件测试网Q RJ;{ad
//pObj是CMyClassTester类的成员变量,是被测试类的对象的指针,
j(AA%x'^pQ-\0 //为求简单,所有的测试类都可以用pObj命名被测试对象的指针。51Testing软件测试网z@rR T w|?6k&R
pObj = new CMyClass();
DRAz8qlO'@0}
` G1t?}0N^|.hP0
*x@!a(K1u8B@.?1^%ON0void CMyClassTester::CaseEnd()51Testing软件测试网%gX:K3A)XP\'`
{
.R,U;PNt9|5R3n e0 delete pObj;51Testing软件测试网 U.]/S EYS
}51Testing软件测试网r:l*mT ?w
测试类的函数CaseBegin()和CaseEnd()建立和销毁被测试对象,每个测试用例的开头都要调用CaseBegin(),结尾都要调用CaseEnd()。51Testing软件测试网 bI;b$\'glsb"f(m
5nA.k/giyp0接下来,我们建立示例的产品函数:
T(O9{TL g~0int CMyClass::Add(int i, int j)51Testing软件测试网7ty$n6Hrm'Sd(Q
{51Testing软件测试网7Bo1c$J:VEL
return i+j;51Testing软件测试网 t|x9Q8A-[-s
}
E v0Ow G R&c,RN;CC0和对应的测试函数:51Testing软件测试网lT2{,NJR2]W}"I:lo
void CMyClassTester::Add_int_int()
v/R `/N)o\F'O;L.p Y+U0{
V6|%cP`T+R0}
]'A-bPu0把参数表作为函数名的一部分,这样当出现重载的被测试函数时,测试函数不会产生命名冲突。下面添加测试用例:51Testing软件测试网`kh? P:tD
void CMyClassTester::Add_int_int()51Testing软件测试网"k$V q\[7[m}fW
{
]QQ AMHk0 //第一个测试用例
.V:Hy'Z JP^5X;l0 CaseBegin();{ //151Testing软件测试网MfR`@'N
int i = 0; //251Testing软件测试网3P i9qID e0W
int j = 0; //351Testing软件测试网 V[8F;^]0?]k"P3O!U l
int ret = pObj->Add(i, j); //451Testing软件测试网w,p/W4SA*zq
ASSERT(ret == 0); //551Testing软件测试网D ],qa+h5law
}CaseEnd(); //6
%cE%_ud-wI.p0}
bX#}eb&i k|0第1和第6行建立和销毁被测试对象,所加的{}是为了让每个测试用例的代码有一个独立的域,以便多个测试用例使用相同的变量名。51Testing软件测试网 U7aS6e(h
第2和第3行是定义输入数据,第4行是调用被测试函数,这些容易理解,不作进一步解释。第5行是预期输出,它的特点是当实际输出与预期输出不同时自动报错,ASSERT是VC的断言宏,也可以使用其他类似功能的宏,使用测试工具进行单元测试时,可以使用该工具定义的断言宏。51Testing软件测试网^+q`w"so&j7D
u o&Nle(ul,w8o0 示例中的格式显得很不简洁,2、3、4、5行可以合写为一行:ASSERT(pObj->Add(0, 0) == 0);但这种不简洁的格式却是老纳极力推荐的,因为它一目了然,易于建立多个测试用例,并且具有很好的适应性,同时,也是极佳的代码文档,总之,老纳建议:输入数据和预期输出要自成一块。51Testing软件测试网s%s\ Z9g(NUT
建立了第一个测试用例后,应编译并运行测试,以排除语法错误,然后,使用拷贝/修改的办法建立其他测试用例。由于各个测试用例之间的差别往往很小,通常只需修改一两个数据,拷贝/修改是建立多个测试用例的最快捷办法。
Yl8W?*Xhb0
%BjD)c;u0三 测试用例51Testing软件测试网`N-~9Q8CFV#U
下面说说测试用例、输入数据及预期输出。输入数据是测试用例的核心,老纳对输入数据的定义是:被测试函数所读取的外部数据及这些数据的初始值。外部数据是对于被测试函数来说的,实际上就是除了局部变量以外的其他数据,老纳把这些数据分为几类:参数、成员变量、全局变量、IO媒体。IO媒体是指文件、数据库或其他储存或传输数据的媒体,例如,被测试函数要从文件或数据库读取数据,那么,文件或数据库中的原始数据也属于输入数据。一个函数无论多复杂,都无非是对这几类数据的读取、计算和写入。预期输出是指:返回值及被测试函数所写入的外部数据的结果值。返回值就不用说了,被测试函数进行了写操作的参数(输出参数)、成员变量、全局变量、IO媒体,它们的预期的结果值都是预期输出。一个测试用例,就是设定输入数据,运行被测试函数,然后判断实际输出是否符合预期。下面举一个与成员变量有关的例子:
i4H/h4N$xL0产品函数:
|3Bpt:lBfa0void CMyClass::Grow(int years)
'T*L3KG _0{51Testing软件测试网hO5h8\M v`)C
mAge += years;
?[(M7xb0
l/m;~ d0F*b[0 if(mAge < 10)51Testing软件测试网1Tk1fmH|t$m
mPhase = "儿童";
x)M r/sW'X0 else if(mAge <20)
\@Wz&g
工厂在组装一台电视机之前,会对每个元件都进行测试,这,就是单元测试。51Testing软件测试网+\6U:Foy;L
其实我们每天都在做单元测试。你写了一个函数,除了极简单的外,总是要执行一下,看看功能是否正常,有时还要想办法输出些数据,如弹出信息窗口什么的,这,也是单元测试,老纳把这种单元测试称为临时单元测试。只进行了临时单元测试的软件,针对代码的测试很不完整,代码覆盖率要超过70%都很困难,未覆盖的代码可能遗留大量的细小的错误,这些错误还会互相影响,当BUG暴露出来的时候难于调试,大幅度提高后期测试和维护成本,也降低了开发商的竞争力。可以说,进行充分的单元测试,是提高软件质量,降低开发成本的必由之路。51Testing软件测试网,TE G3S*c
对于程序员来说,如果养成了对自己写的代码进行单元测试的习惯,不但可以写出高质量的代码,而且还能提高编程水平。
#vx!a~2Fj k0 要进行充分的单元测试,应专门编写测试代码,并与产品代码隔离。老纳认为,比较简单的办法是为产品工程建立对应的测试工程,为每个类建立对应的测试类,为每个函数(很简单的除外)建立测试函数。首先就几个概念谈谈老纳的看法。51Testing软件测试网h x|*gBB(zK/A
一般认为,在结构化程序时代,单元测试所说的单元是指函数,在当今的面向对象时代,单元测试所说的单元是指类。以老纳的实践来看,以类作为测试单位,复杂度高,可操作性较差,因此仍然主张以函数作为单元测试的测试单位,但可以用一个测试类来组织某个类的所有测试函数。单元测试不应过分强调面向对象,因为局部代码依然是结构化的。单元测试的工作量较大,简单实用高效才是硬道理。
y:T(X3O}+t y}T0 有一种看法是,只测试类的接口(公有函数),不测试其他函数,从面向对象角度来看,确实有其道理,但是,测试的目的是找错并最终排错,因此,只要是包含错误的可能性较大的函数都要测试,跟函数是否私有没有关系。对于C++来说,可以用一种简单的方法区隔需测试的函数:简单的函数如数据读写函数的实现在头文件中编写(inline函数),所有在源文件编写实现的函数都要进行测试(构造函数和析构函数除外)。
l A*dz4L0 什么时候测试?单元测试越早越好,早到什么程度?XP开发理论讲究TDD,即测试驱动开发,先编写测试代码,再进行开发。在实际的工作中,可以不必过分强调先什么后什么,重要的是高效和感觉舒适。从老纳的经验来看,先编写产品函数的框架,然后编写测试函数,针对产品函数的功能编写测试用例,然后编写产品函数的代码,每写一个功能点都运行测试,随时补充测试用例。所谓先编写产品函数的框架,是指先编写函数空的实现,有返回值的随便返回一个值,编译通过后再编写测试代码,这时,函数名、参数表、返回类型都应该确定下来了,所编写的测试代码以后需修改的可能性比较小。
!kYamK'iz]0 由谁测试?单元测试与其他测试不同,单元测试可看作是编码工作的一部分,应该由程序员完成,也就是说,经过了单元测试的代码才是已完成的代码,提交产品代码时也要同时提交测试代码。测试部门可以作一定程度的审核。51Testing软件测试网~M7Oy]?
关于桩代码,老纳认为,单元测试应避免编写桩代码。桩代码就是用来代替某些代码的代码,例如,产品函数或测试函数调用了一个未编写的函数,可以编写桩函数来代替该被调用的函数,桩代码也用于实现测试隔离。采用由底向上的方式进行开发,底层的代码先开发并先测试,可以避免编写桩代码,这样做的好处有:减少了工作量;测试上层函数时,也是对下层函数的间接测试;当下层函数修改时,通过回归测试可以确认修改是否导致上层函数产生错误。
S+{!wM"}P;o5mz&cNW0
9e?$p.{"}@&c r}0二 测试代码编写
)BHSM_y0 多数讲述单元测试的文章都是以Java为例,本文以C++为例,后半部分所介绍的单元测试工具也只介绍C++单元测试工具。下面的示例代码的开发环境是VC6.0。
K+I:eJ'f0
C \}+^$C2d0产品类:51Testing软件测试网4]Rp+kV$IA
class CMyClass
u-E P*nr0L Di^;`I0{51Testing软件测试网&c!~d~5N'P
public:51Testing软件测试网 Z"SU%~{)X~!w
int Add(int i, int j);51Testing软件测试网,sY^/JK
CMyClass();
c7~ZOE,~ ue)s0 virtual ~CMyClass();
Ye4Y%{~&r1Z@m051Testing软件测试网3~ aFz D4i-sw
private:
0r-PDT[&Co A0 int mAge; //年龄
#q$t5EMu5?2m0 CString mPhase; //年龄阶段,如"少年","青年"51Testing软件测试网%[5kDQlF|
};51Testing软件测试网_+V.bS5RK
'bZ s;] i5nhB0建立对应的测试类CMyClassTester,为了节约编幅,只列出源文件的代码:51Testing软件测试网c$j j}ZU@
void CMyClassTester::CaseBegin()
.y;j7L\7Q3hBv0{51Testing软件测试网Q RJ;{ad
//pObj是CMyClassTester类的成员变量,是被测试类的对象的指针,
j(AA%x'^pQ-\0 //为求简单,所有的测试类都可以用pObj命名被测试对象的指针。51Testing软件测试网z@rR T w|?6k&R
pObj = new CMyClass();
DRAz8qlO'@0}
` G1t?}0N^|.hP0
*x@!a(K1u8B@.?1^%ON0void CMyClassTester::CaseEnd()51Testing软件测试网%gX:K3A)XP\'`
{
.R,U;PNt9|5R3n e0 delete pObj;51Testing软件测试网 U.]/S EYS
}51Testing软件测试网r:l*mT ?w
测试类的函数CaseBegin()和CaseEnd()建立和销毁被测试对象,每个测试用例的开头都要调用CaseBegin(),结尾都要调用CaseEnd()。51Testing软件测试网 bI;b$\'glsb"f(m
5nA.k/giyp0接下来,我们建立示例的产品函数:
T(O9{TL g~0int CMyClass::Add(int i, int j)51Testing软件测试网7ty$n6Hrm'Sd(Q
{51Testing软件测试网7Bo1c$J:VEL
return i+j;51Testing软件测试网 t|x9Q8A-[-s
}
E v0Ow G R&c,RN;CC0和对应的测试函数:51Testing软件测试网lT2{,NJR2]W}"I:lo
void CMyClassTester::Add_int_int()
v/R `/N)o\F'O;L.p Y+U0{
V6|%cP`T+R0}
]'A-bPu0把参数表作为函数名的一部分,这样当出现重载的被测试函数时,测试函数不会产生命名冲突。下面添加测试用例:51Testing软件测试网`kh? P:tD
void CMyClassTester::Add_int_int()51Testing软件测试网"k$V q\[7[m}fW
{
]QQ AMHk0 //第一个测试用例
.V:Hy'Z JP^5X;l0 CaseBegin();{ //151Testing软件测试网MfR`@'N
int i = 0; //251Testing软件测试网3P i9qID e0W
int j = 0; //351Testing软件测试网 V[8F;^]0?]k"P3O!U l
int ret = pObj->Add(i, j); //451Testing软件测试网w,p/W4SA*zq
ASSERT(ret == 0); //551Testing软件测试网D ],qa+h5law
}CaseEnd(); //6
%cE%_ud-wI.p0}
bX#}eb&i k|0第1和第6行建立和销毁被测试对象,所加的{}是为了让每个测试用例的代码有一个独立的域,以便多个测试用例使用相同的变量名。51Testing软件测试网 U7aS6e(h
第2和第3行是定义输入数据,第4行是调用被测试函数,这些容易理解,不作进一步解释。第5行是预期输出,它的特点是当实际输出与预期输出不同时自动报错,ASSERT是VC的断言宏,也可以使用其他类似功能的宏,使用测试工具进行单元测试时,可以使用该工具定义的断言宏。51Testing软件测试网^+q`w"so&j7D
u o&Nle(ul,w8o0 示例中的格式显得很不简洁,2、3、4、5行可以合写为一行:ASSERT(pObj->Add(0, 0) == 0);但这种不简洁的格式却是老纳极力推荐的,因为它一目了然,易于建立多个测试用例,并且具有很好的适应性,同时,也是极佳的代码文档,总之,老纳建议:输入数据和预期输出要自成一块。51Testing软件测试网s%s\ Z9g(NUT
建立了第一个测试用例后,应编译并运行测试,以排除语法错误,然后,使用拷贝/修改的办法建立其他测试用例。由于各个测试用例之间的差别往往很小,通常只需修改一两个数据,拷贝/修改是建立多个测试用例的最快捷办法。
Yl8W?*Xhb0
%BjD)c;u0三 测试用例51Testing软件测试网`N-~9Q8CFV#U
下面说说测试用例、输入数据及预期输出。输入数据是测试用例的核心,老纳对输入数据的定义是:被测试函数所读取的外部数据及这些数据的初始值。外部数据是对于被测试函数来说的,实际上就是除了局部变量以外的其他数据,老纳把这些数据分为几类:参数、成员变量、全局变量、IO媒体。IO媒体是指文件、数据库或其他储存或传输数据的媒体,例如,被测试函数要从文件或数据库读取数据,那么,文件或数据库中的原始数据也属于输入数据。一个函数无论多复杂,都无非是对这几类数据的读取、计算和写入。预期输出是指:返回值及被测试函数所写入的外部数据的结果值。返回值就不用说了,被测试函数进行了写操作的参数(输出参数)、成员变量、全局变量、IO媒体,它们的预期的结果值都是预期输出。一个测试用例,就是设定输入数据,运行被测试函数,然后判断实际输出是否符合预期。下面举一个与成员变量有关的例子:
i4H/h4N$xL0产品函数:
|3Bpt:lBfa0void CMyClass::Grow(int years)
'T*L3KG _0{51Testing软件测试网hO5h8\M v`)C
mAge += years;
?[(M7xb0
l/m;~ d0F*b[0 if(mAge < 10)51Testing软件测试网1Tk1fmH|t$m
mPhase = "儿童";
x)M r/sW'X0 else if(mAge <20)
\@Wz&g