单元测试作业指导书

发表于:2007-05-05来源:作者:点击数: 标签:单元测试测试单元指导书作业
这是我以前任项目经理时,编写的关于 单元测试 方面的作业指导书,针对多种 开发 环境叙述怎么进行单元测试以及环境配置,现在整理了一下。应该对大家有所帮助。 这是第一部分,主要针对C和C++项目的(包括了 Windows 环境和 Linux ');" href="javascript:;" t
这是我以前任项目经理时,编写的关于单元测试方面的作业指导书,针对多种开发环境叙述怎么进行单元测试以及环境配置,现在整理了一下。应该对大家有所帮助。

  这是第一部分,主要针对C和C++项目的(包括了Windows环境和Linux');" href="javascript:;" target=_self>Linux环境),下部分将针对Java及J2EE项目。

  1. 目的

  为了减少代码中的错误数量, 减少调试所花的时间和精力, 改善软件质量, 减少开发和维护的时间和成本。

  2. 适用范围

  适用于C及C++的所有产品。

  3. 适用内容

  3.1 C++标准

  3.1.1测试环境使用Visual C++,Windows窗口应用程序

  3.1.1.1前题:使用CppUnit1.6.2版,解压后,路径为x:\\cppunit-1.6.2;

  在工程文件中配置测试框架使用环境:加入执行头文件的路径x:\\cppunit-1.6.2\include,加入导入库文件的路径x:\\cppunit-1.6.2\lib;

  配置DEBUG(测试)版环境:

  加入需要链接的静态测试框模块testrunnercd.lib(运行测试用例的选择对话框)和cppunitcd.lib(测试框架);

  加入测试Add-ins,库名为x:\\cppunit-1.6.2\lib\TestRunnerDSPlugInD.dll;

  在Project Settings/C++/C++ Language中启用RTTI;

  3.1.1.2建立测试用例:

  1、以类名加前辍“Test”命名测试单元文件名,比如“CMabString”类的类文件名为MabString.cpp,则测试单元文件命名为TestMabString.cpp;

  2、加入测试框架头文件以及要测试的单元头文件,以TestMabString为例:

  头文件:testmabstring.h clearcase/" target="_blank" >cc66 width="90%" align=center bgColor=#eefffe border=1>
#ifndef CPP_UNIT_TestNode_H
#define CPP_UNIT_TestNode_H
//包含测试框架的头文件
#include <cppunit/TestCase.h>
#include <cppunit/extensions/HelperMacros.h>
//包含被测试单元的头文件
#include "mabstring.h"
//派生测试框架的测试用例类
class TestMabString : public CppUnit::TestCase
{
 //定义测试用例列表,此列表将出现在运行测试用例的选择对话框中
 CPPUNIT_TEST_SUITE( TestMabString );
 CPPUNIT_TEST( FindByName );
 CPPUNIT_TEST_SUITE_END();

 protected:
 //
 CMabString m_MabStr;
 public:
  //用例初始化,可作为桩函数
  void setUp ();
  //用例析构
  void tearDown();
 protected:
  //测试用例
  void FindByName (void);
};

#endif

类文件:testmabstring.cpp

#include "TestMabString.h"
#include "iostream.h"
#include "strstrea.h"

//注册本测试单元

CPPUNIT_TEST_SUITE_REGISTRATION( TestMabString );

//定义测试用例

void TestMabString::FindByName ()
{
 //功能性测试,属黑盒测试
 //normal test
 //条件及错误测试,属白盒测试
 //extra test,
 //例外测试,属白盒测试
 //exception test,

 bool bRet=false;
 try{
  //put the exception code here...
 }
 //catch(CXXX& e)
 catch(...)
 {
  bRet=true;
 }
 CPPUNIT_ASSERT(bRet);
 //由于并不能够执行所有单元测试应该执行的路径,比如CMabString是从CString
 //类中派生出来的,而可能CMabString中的Find只简单调用了CString中的Find方法,//所以并不需要测试;
 //在此处说明所有不用测试的路径;
 //other test, see the ...
}

void TestMabString::setUp ()
{
 //开始测试前的初始代码
 m_pNode=new Node();
}

void TestMabString::tearDown()
{
 //测试结束代码
 if(m_pNode)
  delete m_pNode;
}

  3、在启动程序中加入以下代码,以便运行“测试用例选择”对话框:
#ifdef _DEBUG

//包括测试头文件

#include <msvc6/testrunner/TestRunner.h>

#include <cppunit/extensions/TestFactoryRegistry.h>

static AFX_EXTENSION_MODULE extTestRunner;

#endif



//以下为测试代码,此部分测试不会出现在发布版中

#ifdef _DEBUG

TestRunner runner;

runner.addTest ( CppUnit::TestFactoryRegistry::getRegistry().makeTest() );

runner.run ();

#endif

  4、制作发行版

  发行版需要做以下工作

  将Project的属性设置为Release(这将自动去除_DEBUG的声明);

  从工程项目中去掉测试文件(即带有test前辍的文件);

  3.1.2测试环境使用Visual C++,Windows非窗口应用程序

  3.1.2.1前题:使用CppUnit1.6.2版,解压后,路径为x:\\cppunit-1.6.2;

  在工程文件中配置测试框架使用环境:加入执行头文件的路径x:\\cppunit-1.6.2\include,加入导入库文件的路径x:\\cppunit-1.6.2\lib;

  配置DEBUG(测试)版环境:

  加入需要链接的静态测试框模块cppunitcd.lib(测试框架);

  在Project Settings/C++/C++ Language中启用RTTI;

  3.1.2.2建立测试用例:

  1、以类名加前辍“Test”命名测试单元文件名,比如“CMabString”类的类文件名为MabString.cpp,则测试单元文件命名为TestMabString.cpp;

  2、加入测试框架头文件以及要测试的单元头文件,以TestMabString为例:

  头文件:testmabstring.h

  3、测试示例同上;

  3.2 C标准

  3.2.1测试环境使用gcc,Linux非窗口应用程序

  前题:使用check0.8.0版,解压后,路径为/xx/check-0.8.0;

  配置测试框架使用环境(我建议采用标准组织推荐的使用Autoconf和Automake来生成配置文件configure和Makefile,因为使用它们可以建立符合国际标准的configure脚本 和Makefile文件,并且可以有效的建立压缩包和方便分发必需的文件(也方便在发行版中去除测试用例文件):

  l 首先需编写configure.in文件,此文件用于Autoconf生成configure可执行脚本;configure.in的框架大致如下:

  dnl 此文件用于生成configure脚本,

  dnl AC_INIT的xxxx.h参数代表本目录下一个有效的文件名

  AC_INIT(xxxx.h)

  dnl AM_INIT_AUTOMAKE的两个参数分别是生成应用程序的版本及版本号,

  dnl 可能有些版本的Autoconf和Automake不支持此宏

  AM_INIT_AUTOMAKE(xxxx, x.x)

  dnl 以下为编译依赖的检测

  dnl Checks for programs.

  AC_PROG_AWK

  AC_PROG_CC

  AC_PROG_INSTALL

  AC_PROG_LN_S

  dnl Checks for libraries.

  AC_CHECK_LIB(check,suite_create)

  dnl Checks for header files.

  AM_CONFIG_HEADER(config.h)

  dnl Checks for typedefs, structures, and compiler characteristics.

  dnl Checks for library functions.

  dnl 将Automake生成的Makefile.in文件输出为Makefile文件

  AC_OUTPUT(Makefile)

  (提示:autoscan可以生成configure.in文件的基本框架,但很基本,可其生成的configure.scan文件的基础补充,然后更名为configure.in)

  l 编写Makefile.am文件,用于Automake生成Makefile.in文件,Makefile.am文件的大致框架如下:(其中xxxx为应用程序文件名,比如program.c文件的测试程序文件名我建议为check_program.c;)

TESTS = check_xxxx

noinst_PROGRAMS=check_xxxx

frame_path=xx/check-0.8.0

xxxx_docs =\

srcfilelist_1\

srcfilelist_2\

.......\

.....

xxxx_SOURCES=\

srcfilelist_1\

srcfilelist_2\

.......

EXTRA_DIST = $(xxxx_docs)
INCLUDES = -I$(frame_path)/src -I$(other_path)/include

LDADD= \$(frame_path)/src/libcheck.a

CLEANFILES=*.*~

  (Makefile.am有很许多标记,可以参阅相应文档。但常用的如:noinst_PROGRAMS为生成的可执行文件,xxxx_SOURCES(应用程序名加后辍_SOURCES)为源文件列表,EXTRA_DIST为发布程序时不需要的文件列表(用此方法可以将测试文件去掉),INCLUDES为要包含的头文件路径,check的头文件位置在其安装目录下的src中;LDADD为要链接的库文件名,libcheck.a为check测试框架的库文件;)

   使用Automake –a –-foreign来生成Makefile.in文件,--foreign是为了生成几个外部文件如install.sh等,如果已有这些文件则可以省略这个参数;

   使用Autoconf来生成configure执行脚本;然后执行./configure来生成Makefile文件;

   执行make来生成可执行程序;

  3.2.2 建立测试用例:

  1、以程序文件名加前辍“check_”命名测试单元文件名,比如money.c文件的测试单元文件命名为check_money.c;

  2、加入测试框架头文件以及要测试的单元头文件,以check_money为例:

  头文件:money.h;源文件:money.c;测试单元文件:check_money.c:

  测试文件框架如下:

#include <stdlib.h>

#include <check.h>

#include "money.h"

/*建立必要的测试变量,Money为money.h中定义的结构struct money*/

Money *five_dollars;

/*单元测试初始化函数*/

void setup (void)

{

five_dollars = money_create(5, "USD");

}

/*单元测试结束函数*/

void teardown (void)

{

money_free (five_dollars);

}


/*单元测试用例,用例名为test_create*/

/*test functions: money_amout()*/

START_TEST(test_create)

{

/*功能性测试,属黑盒测试*/

/*normal test*/

fail_unless (money_amount(five_dollars) = = 5,

"Amount not set correctly on creation");

fail_unless (strcmp(money_currency(five_dollars),"USD") = = 0,

"Currency not set correctly on creation");

/*条件及错误路径测试,属白盒测试*/

/*extra test*/

}

END_TEST


/*单元测试用例,用例名为test_net_create*/

START_TEST(test_neg_create)

{

Money *m = money_create(-1, "USD");

fail_unless (m = = NULL, "NULL should be returned on attempt to create with a negative amount");

}

END_TEST



/*单元测试用例,用例名为test_net_create*/

START_TEST(test_zero_create)

{

Money *m = money_create(0, "USD");

fail_unless (money_amount(m) = = 0,

"Zero is a valid amount of money");

}

END_TEST



/*单元测试组装,将所有单元测试组装到一个“箱子”里面,“箱子”名为Money*/

Suite *money_suite (void)

{

Suite *s = suite_create("Money");



/*测试用例分组*/

TCase *tc_core = tcase_create("Core");

TCase *tc_limits = tcase_create("Limits");



/*将分组加入“箱子”

suite_add_tcase (s, tc_core);

suite_add_tcase (s, tc_limits);



/*分别将不同用例加入分组*/

tcase_add_test (tc_core, test_create);

tcase_add_checked_fixture (tc_core, setup, teardown); /*此用例注册初始化和结束函数*/

/*以下用例将不注册初始化和结束函数*/

tcase_add_test (tc_limits, test_neg_create);

tcase_add_test (tc_limits, test_zero_create);

return s;

}



/*执行测试用例*/

int main (void)

{

int nf;

Suite *s = money_suite();

SRunner *sr = srunner_create(s);

srunner_run_all (sr, CK_NORMAL);

nf = srunner_ntests_failed(sr);

srunner_free(sr);

suite_free(s);

return (nf == 0) ? EXIT_SUCCESS : EXIT_FAILURE;

}

  3.2.3 制作发行版:

  制作发行版只须配置另外一份Makefile.am,在此文件中的源文件列表加入执行主体,即应用程序包含main函数的文件;也可在制作测试版的Makefile.am中加入发行版的配置,这样就可以直接生成测试版程序和发行版程序。

3.3 JAVA标准

3.3.1测试环境使用JSDK1.3

3.3.1.1前题:

使用JUnit3.7版,解压后,路径为x:\\junit3.7;

使用Ant1.3版,解压后,路径为x:\\ant1.3;

配置测试框架使用环境:

测试框架使用环境可以在build.xml文件中配置,此文件是ant的输入文件,ant根据其来生成和执行应用程序;(这相当于C/C++的Makefile文件,但比之功能要强大、且配置更为简单);

1.x:\\ant1.3\bin目录下含有ant执行程序的批处理文件,所以要将此目录加入到系统环境变量path中;

2.建立项目文件目录结构;我建议在进行项目开发能有一个完整的目录结构,如下所示:

“AntCons”为项目目录名称,可以替换为所需要的项目名称;在项目目录下有两个子目录分别为build和src,这两个子目录分别为源文件目录和发布文件目录;而src(源文件目录)又分为main(源文件)和test(测试文件)目录。而com.myjava则是包名,对应于源java文件中的package;

3.在项目目录中建立build.xml文件(未涉及测试的),文件大致结构如下所示:

定义项目名称及位置

首先声明一些变量,便于使用

定义目标:编译;

在此目标中将建立发布目录、编译目标java源文件并且指定class路径;

classpath="${java.class.path};${extclasslib}">

定义目标:运行;

此目标依赖于编译目标;

(build.xml相关标记的说明可以在ant下载包的说明文档中找到,ant定义了很多方便操作的指令)

4.编写代码文件,我们假设代码文件位于com.myjava包中,则代码文件将放置于src/main/com/myjava目录下;

5.编写单元测试文件,依照上面的项目目录设置,则单元测试文件放置于src/test下;

3.3.1.2建立测试用例:

1、以类名加前辍“Test”命名测试单元文件名,比如“Sample”类的类文件名为Sample.java,则测试单元文件命名为TestSample.java;

2、编写测试用例类,在测试用例类中引入测试框架包以及要测试的包声明,以TestSample为例:

测试文件TestSample.java:

package test.Sample; //测试包声明

import junit.framework.*; //引入测试框架包

import xxxx; //引入被测试单元包

//派生测试框架的测试用例类

public class TestSimple extends TestCase {

protected int fValue1;

protected int fValue2;

public TestSimple(String name) {

super(name);

}

//用例初始化,可作为桩函数

protected void setUp() {

fValue1= 2;

fValue2= 3;

}

//用例析构

protected void tearDown(){

}

//组装测试用例

public static Test suite() {

/*

* the type safe way

*

//增加测试用例到“箱子”

TestSuite suite= new TestSuite();

suite.addTest(

new TestSimple("add") {

protected void runTest() { testAdd(); }

}

);

suite.addTest(

new TestSimple("testDivideByZero") {

protected void runTest() { testDivideByZero(); }

}

);

return suite;

*/

/*

* the dynamic way

*/

return new TestSuite(TestSimple.class);

}

//用例1:

public void testAdd() {

//normal test, 功能测试,属黑盒测试

double result= fValue1 + fValue2;

assertTrue(result == 6);

//extra test, 条件及错误路径测试,属白盒测试

//except test, 异常测试,属白盒测试

//other test, see the functions "...",

//其它测试,比如被测试类的基类方法,在此需要做出说明

}

//用例2:

public void testDivideByZero() {

//................

}

//用例3:

public void testEquals() {

//.................

}

}

3. 编写驱动测试用例类,将各个测试用例组装到一起进行测试:

package test.Sample; //测试包声明

import junit.framework.*; //引入测试框架包

import xxxx; //引入被测试单元包

/**

* TestSuite that runs all the sample tests

*

*/

public class AllTests {

public static void main (String[] args) {

junit.textui.TestRunner.run (suite());

}

public static Test suite ( ) {

//建立测试用例的“箱子”

TestSuite suite= new TestSuite("All Tests");

suite.addTest(TestSimple.suite());

//此处增加一些其它单元测试类

//......

suite.addTest(junit.tests.AllTests.suite());

return suite;

}

}

4. 将测试单元的编译及运行加入到build.xml中;

在build.xml的project标记加入两个目标,现在的build.xml如下所示:

定义项目名称及位置

首先声明一些变量,便于使用

定义目标:依赖库检测;

定义目标:编译;

在此目标中将建立发布目录、编译目标java源文件并且指定class路径;

classpath="${java.class.path};${extclasslib}">

定义目标:打包;

在此目标中将建立分发包目录、将编译完成的.class文件打包为jar文件;

basedir="${builddir}" includes="com/**"/>

定义目标:编译测试用例;

testcases />

定义目标:运行应用程序;

此目标依赖于编译目标;

定义目标:运行测试用例;

在此目标中将运行测试用例,运行在窗口中进行还是在控制台进行取决于所用classname,"junit.textui.TestRunner"表示在控制台进行,如果改为"junit.ui.TestRunner"则可以在窗口中运行

taskname="junit" failonerror="true">


5. 最后,在项目所在目录执行Ant target_name,比如:运行测试用例可以使用Ant runtests;

3.3.2 使用Junit测试EJB

3.3.2.1安装JUnit和JunitEE

你需要将junit.jar和junitee.jar加入到J2EE应用程序的CLASSPATH中。一般地,将jar文件拷贝到服务器的根目录下的lib目录中就可以了。注意:该目录下的文件一般不能被动态加入,所以你需要重新启动应用服务器

3.3.2.2准备一个EJB例子

你可以在JunitEE安装目录的example子目录下找到一个作为示例的EJB。你要先组织该示例,保证它是可以工作的。否则,注意发生了什么事情。你要编辑build.xml文件按照你计算机的具体情况更新文件中的路径。然后在build文件所在的目录下运行“ant”。
《something》

运行该例子的结果是得到名为junitee-example.ear的文件。该文件在名为out的子目录中。它包含一个简单的无状态EJB,一个使用了EJB的应用程序和测试web应用程序。这是一个标准的J2EE企业级的文档,可以安装在任何的应用服务器上。

如果你想手工的写出所有的部分,就删除example目录下的components/test-war子目录,本文下面的部分就是说明如何构建测试。

3.3.2.3创建目录结构

要创建一个Web应用程序测试,首先你要创建目录结构。创建下面的子目录:
example/javasrc/org/infohazard/test/
example/components/test-war/
example/components/test-war/WEB-INF/

本示例将测试用例放置在org.infohazard.test包中。如果你想测试包内的私有方法,则要将测试用例和要测试的class文件放置在同一个包中。由于绝大多数的测试发生在public接口层,所以你要考虑是不是要这样做。

3.3.2.4编写测试用例

测试用例是标准的JUnit测试用例。对于fixture,你可以使用缺省的JNDI InitialContext来得到EJB的索引,如下:

protected void setUp() throws Exception
{
Context jndiContext = new InitialContext();
Object einRef = jndiContext.lookup("java:comp/env/ejb/EinsteinEJB");
EinsteinHome home = (EinsteinHome)PortableRemoteObject.narrow(einRef, EinsteinHome.class);
this.ein = home.create();
}

测试方法类似于:

public void testSimpleAddition() throws RemoteException
{
String result = this.ein.addTwoNumbers("7", "10");
assert(result.equals("17"));
}

这些代码属于example/javasrc/org/infoazard/test/EinsteinTest.java

3.3.2.5使用 servlet来运行测试用例

下面是一个包括在junit.htmlui包中的servlet,但是你不可以直接使用它。你必须在你的web应用程序的WEB-INF/classes/下创建一个servlet,这个servlet来源于junit.htmlui.TestServletBase并且增加了下面的细节:

protected ClassLoader getDynamicClassLoader()
{
return this.getClass().getClassLoader();
}

使用这个servlet,我们使用动态的类载入器欺骗应用服务器,这样不必使用缺省的类载入。如果我们不这样做,每次改变的测试类文件都要重新启动web应用服务器。

如果你的应用程序服务器不能动态地重新载入修改过的class文件,这一步对你来说是没有帮助的。你不得不重新启动服务器,因为TestServletBase被声明为abstract类型。

3.3.2.6创建UI表格

<p>
You may type in the name of a test suite:
<br/>
<form action="TestServlet" method="get" name="youTypeItForm">
<input type="text" name="suite" size=60 />
<input type="submit" value="Run" />
</form>
</p>
<hr/>
<p>
You may pick one or more of the following test suites:
<br/>
<form action="TestServlet" method="get" name="youPickItForm">
<select name="suite" size="2" multiple>
<option value="org.infohazard.test.EinsteinTest">
org.infohazard.test.EinsteinTest
</option>
<option value="some.other.Test">
some.other.Test
</option>
</select>
<input type="submit" value="Run" />
</form>
</p>

将此保存为example/components/test-war/index.html文件。

3.3.2.7 创建web.xml配置描述器

Web应用程序必须有一个配置描述器,它提供了ejb-ref映射,使得"java:comp/env/ejb/EinsteinEJB" JNDI lookup生效,and so that the TestServlet gets mapped to some sort of URI.下面是web.xml的示例:

<?xml version="1.0"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
<web-app>
<display-name> Einstein Unit Tester Web Application </display-name>
<servlet>
<servlet-name>JUnitEETestServlet</servlet-name>
<description>JUnitEE test framework</description>
<servlet-class>org.infohazard.servlet.TestServlet</servlet-class>
</servlet>
<ejb-ref>
<ejb-ref-name>ejb/EinsteinEJB</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<home>org.infohazard.ejb.einstein.EinsteinHome</home>
<remote>org.infohazard.ejb.einstein.Einstein</remote>
</ejb-ref>

<servlet-mapping>
<servlet-name>JUnitEETestServlet</servlet-name>
<url-pattern>/TestServlet</url-pattern>
</servlet-mapping>
</web-app>

保存成:example/components/test-war/WEB-INF/web.xml。

3.4 单元测试完毕后,程序员将测试用例交与项目经理,由项目经理进行单元测试的检查,完成单元测试报告,对测试的情况进行总结说明。

注意:以上所述仅是测试J2EE的一种方式而已,我们还可以结合HttpUnit和Jakarta Cataus来进行J2EE项目的单元测试。这些完全借助于工具的方式还是有一定的缺陷,所以在去年的时候我根据XP中的Mock Object理论的一些指导,在HttpUnit的基础上编写了一套完全脱离J2EE运行环境(比如Weblogic等)的单元测试套件,以及压力测试功能测试套件,在这一组工具的支持下才算作好了J2EE项目的单元测试。

原文转自:http://www.ltesting.net