2005年五月月 发布的文章

CppUnit入门实践-Tony与Alex的对话系列

Tony : Hi Alex ! you just looks like drowing in your project. what is up?
Alex : 我们的项目要求引入单元测试,but i've no experience in unit test.
Tony : i think cppunit is your best choice.
Alex : 是的,我刚从网上把它down了下来,正准备研究它呢。
Tony : Really ? I have done some practice on unit test before. would you like me to join you?
Alex : Oh Tony, I'm so glad that you could help me !
Tony : My pleasue !
Alex : 我们从哪里开始呢?
Tony : The simplest case! 我们拿一个最简单的例子吧。now we have a class with the name "SimpleCalculator" and it has four basic methods 'add', 'sub', 'mul' and 'div', All we should do is to test whether these methods run as same as we expect. First of all , complete the "SimpleCalculator" class, Alex.
Alex : It is simple!

//SimpleCalculator.h
class SimpleCalculator{
 public :
  int add(int a, int b);
  int sub(int a, int b);
  int mul(int a, int b);
  int div(int a, int b);
};

//SimpleCalculator.cpp
int SimpleCalculator::add(int a, int b){
 return a+b;
}

int SimpleCalculator::sub(int a, int b){
 return a-b;
}

int SimpleCalculator::mul(int a, int b){
 return a*b;
}

int SimpleCalculator::div(int a, int b){
 return a/b;
}

Alex : 这里简单点,div方法没有考虑0作除数的异常情况。
Tony : 可以。还记得我上次讲的测试驱动开发么,不过今天我们不是用它,我们只做些简单的东西,目的就是为了熟悉工具的使用。
Alex : 那我们是不是也应该列出一个test case的list亚?
Tony : 没错。
Alex : 我来随意写几个吧。“add(5,6) == 11” 、“sub(5,6)==-1”、“mul(5,6) == 30”和“div(12,6) == 2”。
Tony : 然后我们一起来学习一下CppUnit的帮助文档吧。

(Tony and Alex are reading the doc of cppunit.)

Tony : 学到了些什么?
Alex : 看来这些xUnit框架的测试工具在概念上几乎是一致的,像TestCase、TestFixture和TestSuite这些概念都大同小异。
Tony : 不错,单元测试在于测试思想,工具只是个必要条件而已,工具并不能决定你的测试就是一个好的测试。下面你就按你理解的CppUnit去做吧。

Alex : Ok. 按照书中所说,我们一次要测试多个method,最好使用TestFixture。我是这样写的,你看看。

#include "SimpleCalculator.h"
#include "CppUnit/TestCase.h"
#include "CppUnit/TestResult.h"
#include "CppUnit/TextOutputter.h"
#include "CppUnit/TestResultCollector.h"
#include "CppUnit/TestCaller.h"
#include "CppUnit/extensions/HelperMacros.h"

class SimpleCalcTest : public CPPUNIT_NS::TestFixture{
private :
 SimpleCalculator * sc;

public:
 virtual void setUp(){
         sc = new SimpleCalculator();
     }
     virtual void tearDown(){
         delete sc;  
     }
 
 void testAdd(){       
         CPPUNIT_ASSERT_EQUAL( sc->add(5,6), 11);
     }

 void testSub(){       
         CPPUNIT_ASSERT_EQUAL( sc->sub(5,6), -1 );
     }

     void testMul(){       
         CPPUNIT_ASSERT_EQUAL( sc->mul(5,6), 30 );
     }

 void testDiv(){       
         CPPUNIT_ASSERT_EQUAL( sc->div(12,6), 2 );
     }
};

我们的主函数如下:
int main()
{
    CPPUNIT_NS::TestResult r;
    CPPUNIT_NS::TestResultCollector result;
    r.addListener( &result );

    CPPUNIT_NS::TestCaller testCase1( "testAdd", &SimpleCalcTest::testAdd );
    CPPUNIT_NS::TestCaller testCase2( "testSub", &SimpleCalcTest::testSub );
    CPPUNIT_NS::TestCaller testCase3( "testMul", &SimpleCalcTest::testMul );
    CPPUNIT_NS::TestCaller testCase4( "testDiv", &SimpleCalcTest::testDiv );
   
    testCase1.run( &r );
    testCase2.run( &r );
    testCase3.run( &r );
    testCase4.run( &r );
   
    CPPUNIT_NS::TextOutputter out( &result, std::cout );
    out.write();
    return 0;
}

Tony : 我觉得可行。运行一下,看看如何。
Alex : 输出结果如下:
OK (4 tests)

Tony : 这的确是一种可行的办法,不过你回想一下我们一起学习的doc中的内容,看看是否还有改进的余地了。现在如果你要在SimpleCalcTest类中加一个测试用例方法,不仅仅SimpleCalcTest要修改,我们的main函数也需要修改,还记得JUnit中有什么概念来支持么?

Alex : 你不提我还真的记不起来了,JUnit中有TestSuite,刚才在cppunit doc中我也看到了suite方法,也许会帮得上忙,稍等一下我再翻翻文档….

Alex : 我找到了。的确有更为简单的方法。我修改一下,引用的头文件不变。

class SimpleCalcTest : public CPPUNIT_NS::TestFixture{

    CPPUNIT_TEST_SUITE( SimpleCalcTest );
        CPPUNIT_TEST( testAdd );
        CPPUNIT_TEST( testSub );
        CPPUNIT_TEST( testMul);
        CPPUNIT_TEST( testDiv );       
    CPPUNIT_TEST_SUITE_END();

private :
 SimpleCalculator * sc;

public:
 virtual void setUp(){
         sc = new SimpleCalculator();
     }
     virtual void tearDown(){
         delete sc;  
     }
 
 void testAdd(){       
         CPPUNIT_ASSERT_EQUAL( sc->add(5,6), 11);
     }

 void testSub(){       
         CPPUNIT_ASSERT_EQUAL( sc->sub(5,6), -1 );
     }

     void testMul(){       
         CPPUNIT_ASSERT_EQUAL( sc->mul(5,6), 30 );
     }

 void testDiv(){       
         CPPUNIT_ASSERT_EQUAL( sc->div(12,6), 2 );
     }
};

CPPUNIT_TEST_SUITE_REGISTRATION( SimpleCalcTest );

主函数修改后如下:
int main()
{
    CPPUNIT_NS::TestResult r;
    CPPUNIT_NS::TestResultCollector result;
    r.addListener( &result );

    CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest()->run( &r );
    CPPUNIT_NS::TextOutputter out( &result, std::cout );
    out.write();
    return 0;
}

CppUnit利用宏来解决Suite的问题。在你的TestCase定义里面写入如下的这段代码:
CPPUNIT_TEST_SUITE( YourTestCase );
        CPPUNIT_TEST( testXX);
   …//
CPPUNIT_TEST_SUITE_END();
这段代码实际上是定义了一个函数suite,这个函数返回了一个包含了所有CPPUNIT_TEST定义的测试用例的一个测试集。CPPUNIT_TEST_SUITE_REGISTRATION通过静态注册把这个测试集注册到全局的测试树中,最后通过CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest()生成一个包含所有测试用例的测试并且运行。这样的话,一旦要添加新的测试用例函数,我们只需要修改SimpleCalcTest类即可。

Tony : Well done! Alex你独立解决问题的能力越来越强了。相信做到这你已经心里有底儿了,再往后就是在你的实际项目中摸索CppUnit的使用经验了。

Alex : 呵呵。谢谢夸奖!

关于Tony与Alex的对话系列的一点说明

相信这种形式的小品文大家看到过一些,其中很有名的有“Solmyr和Zero的故事”,如果没记错的话,那个系列文章记录的是两个人Solmyr和Zero之间的技术交流过程,而据说Solmyr和Zero在真实世界中都是有原型的[注1]。对比起Zero的故事系列,我的文章中语言不免有些干涩,形式过于拘泥,呵呵。不过刚开始么,我会一点点地改进的。我的小品文系列与“Zero的故事系列”最大的不同是它仅是记录了我一个人的思维过程。相信读过我这个系列的人都知道Alex和Tony两个主人公,实际上这两个人的原型都是我自己,Alex的疑问和Tony的解答实际上是我自己的“自问自答”。每篇对话实际上记录的都是我解决某个问题整个思维过程。而其中讨论和解决的问题也都是我在工作和学习中遇到的,颇具实践意义。这个系列小品文我会一直继续下去的,但内容可能不是很连贯,涉猎的话题类型也许会很多。希望对大家还是有所帮助的。

BTW,最近发现有很多网友转载我的Blog,我很是欣慰,觉得自己付出的努力得到了大家的认可,但是处于尊重我的劳动成果的考虑,请转载我的文章时注明文章出处,谢谢。All right reserved!^_^

注1:(源自csdn)
“Solmyr和Zero的故事”这个系列是 zero 的作品。“Solmyr的小品文”系列和“Solmyr和Zero的故事”系列本来是Solmyr和 zero 在 PC Home BBS 上“互相攻击”的产物。

一个C++项目的Makefile编写-Tony与Alex的对话系列

Tony : Hey Alex, How are you doing?
Alex : 不怎么样。(显得很消沉的样子)
Tony : Oh , Really ? What is the matter?
Alex : 事情是这样的。最近有一个Unix下的C++项目要求我独自完成,以前都是跟着别人做,现在让自己独立完成,还真是不知道该怎么办,就连一个最简单的项目的Makefile都搞不定。昨晚看了一晚上资料也没有什么头绪。唉!!
Tony : 别急,我曾经有一段时间研究过一些关于Makefile的东西,也许能帮得上忙,来,我们一起来设计这个项目的Makefile。
Alex : So it is a deal。(一言为定)
Tony : 我们现在就开始吧,给我拿把椅子过来。

(Tony坐在Alex电脑的旁边)
Tony : 把你的项目情况大概给我讲讲吧。
Alex : No Problem ! 这是一个“半成品”项目,也就是说我将提供一个开发框架供应用开发人员使用,一个类似MFC的东西。
Tony : 继续。
Alex : 我现在头脑中的项目目录结构是这样的:

APL (Alex's Programming Library)
 -Make.properties
 -Makefile(1)
 -include  //存放头文件
  -Module1_1.h
  -Module1_2.h
  -Module2_1.h
  -Module2_2.h
 -src   //存放源文件
  -Makefile(2)
  -module1
   -Module1_1.cpp
   -Module1_2.cpp
   -Makefile(3)
  -module2
   -Module2_1.cpp
   -Module2_2.cpp
   -Makefile(3)
  -…
  
 -lib  //存放该Project依赖的库文件,型如libxxx.a
 -dist  //存放该Project编译连接后的库文件libapl.a
 -examples  //存放使用该“半成品”搭建的例子应用的源程序
  Makefile(4)
  -appdemo1
   -Makefile(5)
   -src  //存放应用源代码
   -include
   -bin  //存放应用可执行程序
  -appdemo2
   -Makefile(5)
   -src  //存放应用源代码
   -include
   -bin  //存放应用可执行程序
  -…

Tony : I got it!
Alex : 下面我们该如何做呢?
Tony : 我们来分析一下各个Makefile的作用。你来分析一下各个级别目录下的Makefile的作用是什么呢?
Alex : (思考了一会儿)我想应该是这样的吧。
 Makefile(3)负责将其module下的.cpp源文件编译为同名.o文件,同时其phony target "clean"负责删除该目录下的所有.o文件;
 Makefile(2)负责调用src目录下所有module的Makefile文件。
 Makefile(1)负责先调用src中的Makefile生成静态库文件,然后调用examples中的Makefile构建基于该框架的应用。
 至于Make.properties,定义通用的目录信息变量、编译器参数变量和通用的依赖关系。

Tony : 说得很好。我们一点一点来,先从src中每个module下的Makefile着手,就如你所说在每个module下的Makefile负责将该module下的.cpp文件编译为同名的.o文件。
Alex : 好的,我来写吧,这个我还是能搞定的。看下面:
module1下的Makefile如下:

#
# Makefile for module1
#
all : Module1_1.o Module1_2.o

Module1_1.o : Module1_1.cpp
 g++ -c $^ -I ../../include
Module1_2.o : Module1_2.cpp
 g++ -c $^ -I ../../include

clean :
 rm -f *.o

module2下的Makefile如下:

#
# Makefile for module2
#
all : Module2_1.o Module2_2.o

Module2_1.o : Module2_1.cpp
 g++ -c $^ -I ../../include
Module2_2.o : Module2_2.cpp
 g++ -c $^ -I ../../include

clean :
 rm -f *.o

make一下,顺利产生相应的.o文件。

/*=============================================================
Note: 关于$^、$<和$@的用法说明:
$@ — “$@”表示目标的集合,就像一个数组,“$@”依次取出目标,并执于命令。
$^ — 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。
$< — 依赖目标中的第一个目标名字

举例: Module1_1.o Module1_2.o : Module1_1.cpp Module1_2.cpp
则$@ — Module1_1.o Module1_2.o
$^ — Module1_1.cpp Module1_2.cpp
$< — Module1_1.cpp

==============================================================*/

Tony : Well done! 不过发现什么问题了么?
Alex : 什么问题?
Tony : 存在重复的东西。在重构中我们知道如果两个子类中都定义相同的接口函数,我们会将其pull up到基类中。同样我们可以重构我们的Makefile,把一些重复的东西拿到外层去。
Alex : (似乎略微明白了一些)我想有三处重复:a)查找头文件的路径是重复的; b)g++这个字符串可以用一个变量定义代替 c)编译器的编译参数可以也定义到一个变量中。我知道Make工具支持include一个文件,我们就建立一个公用的文件来存放一些通用的东西吧。
Tony : 没错,Just do it.
Alex : 就按我原先的想法,把这些公共的部分放到Make.properties中吧。

#
# Properties for demo's Makefile
#
MAKEFILE  = Makefile

BASEDIR = $(HOME)/proj/demo

####################
# Directory layout #
####################
SRCDIR = $(BASEDIR)/src
INCLUDEDIR = $(BASEDIR)/include
LIBDIR = $(BASEDIRE)/lib
DISTDIR = $(BASEDIR)/dist

####################
# Compiler options #
#    F_ — FLAG    #
####################
CC = g++

# Compiler search options
F_INCLUDE = -I$(INCLUDEDIR)
F_LIB = -L $(LIBDIR)

CFLAGS =
CPPFLAGS = $(CFLAGS) $(F_INCLUDE)

然后修改一下,各个module中的Makefile文件,以module1为例,修改后如下:
#
# Makefile for module1
#
include ../../Make.properties

all : Module1_1.o Module1_2.o

Module1_1.o : Module1_1.cpp
 $(CC) -c $^ $(CPPFLAGS)
Module1_2.o : Module1_2.cpp
 $(CC) -c $^ $(CPPFLAGS)

clean :
 rm -f *.o

Tony : 其实这两个Makefile中还有一个隐含的重复的地方
Alex : 你是指依赖规则么?
Tony : 嗯,这个依赖规则在src中的各个module中都会用得到的。
Alex : 没错,我也是这么想的,我现在就把这个规则抽取出来,然后你来评审一下。我想利用make工具的传统的“后缀规则”来定义通用依赖规则,我在Make.properties加入下面的变量定义:

####################
# Common depends   #
####################
DEPS = .cpp.o

然后还是以module1为例,修改module1的Makefile后如下:
#
# Makefile for module1
#
include ../../Make.properties

all : Module1_1.o Module1_2.o

$(DEPS):
 $(CC) -c $^ $(CPPFLAGS)

clean :
 rm -f *.o

Tony : 基本满足需求。我们可以进行上一个层次的Makefile的设计了。我们来设计Makefile(2)。Alex,你来回顾一下Makefile(2)的作用。
/*=============================================================
Note: 关于后缀规则的说明
后缀规则中所定义的后缀应该是make 所认识的,如果一个后缀是make 所认识的,那么这个规则就是单后缀规则,而如果两个
连在一起的后缀都被make 所认识,那就是双后缀规则。例如:".c"和".o"都是make 所知道。因而,如果你定义了一个规则是
".c.o"那么其就是双后缀规则,意义就是".c"是源文件的后缀,".o"是目标文件的后缀, ".c.o"意为利用.c文件构造同名.o文件。
==============================================================*/

Alex : No Problem! 正如前面说过的Makefile(2)负责调用src目录下所有module子目录下的Makefile文件,并负责将各个module下的.o文件打包为libdemo.a文件放到dist目录中。所以存在简单的依赖关系就是libdemo.a依赖各个module子目录下的.o文件,而前面的Makefile(3)已经帮我们解决了.o文件的生成问题了,即我们只需要逐个在各module子目录下make即可。我的Makefile(2)文件设计如下:
#
# Makefile for src directory
#

include ../Make.properties

TARGET = libdemo.a

####################
#  Subdirs define  #
####################
MODULE1_PATH = module1
MODULE2_PATH = module2
SUBDIRS = $(MODULE1_PATH) $(MODULE2_PATH) 

####################
#  Objects define  #
####################
MODULE1_OBJS = $(MODULE1_PATH)/Module1_1.o $(MODULE1_PATH)/Module1_2.o
MODULE2_OBJS = $(MODULE2_PATH)/Module2_1.o $(MODULE2_PATH)/Module2_2.o
DEMO_OBJS = $(MODULE1_OBJS) $(MODULE2_OBJS)

all : subdirs $(TARGET)
 cp $(TARGET) $(DISTDIR)

subdirs:
 @for i in $(SUBDIRS); do \
  echo    "===>$$i"; \
  (cd $$i &&$(MAKE) -f $(MAKEFILE)) || exit 1; \
  echo    "<===$$i"; \
 done

$(TARGET) : $(DEMO_OBJS)
 ar -r $@ $^

clean:
 @for i in $(SUBDIRS); do \
  echo    "===>$$i"; \
  (cd $$i &&$(MAKE) clean -f $(MAKEFILE)) || exit 1; \
  echo    "<===$$i"; \
 done
 rm -f $(DISTDIR)/$(TARGET)

Tony : Alex你的进步真的是很大,分析问题的能力提高的很快,方法也不错。这个设计的缺点在于一旦新增了一个module子目录,这个Makefile文件就需要改动,不过改起来倒不是很难。有机会可以再想想,使这个Makefile更加通用。

Alex : 我记住了。我们继续么?
Tony : 歇一回吧^_^。

/*=============================================================
Alex and Tony are having a short break.
==============================================================*/

Tony : 你的咖啡味道真不错。
Alex : 这可是朋友从巴西带回来的极品咖啡豆,经过我精心研磨而成的。
Tony : 想不到你在这方面还有研究。
Alex : 呵呵。
Tony : Let's go on 。有了Makefile(2),后面的工作就轻松多了。
Alex : 现在我的信心也很足,我来设计Makefile(1),它负责先调用src中的Makefile生成静态库文件,然后调用examples中的Makefile构建基于该框架的应用。我还是按照Makefile(2)的思路走,看我的Makefile(1):

#
# Makefile for whole project
#

include Make.properties

SRC_PATH = src
EXAMPLES_PATH = examples

SUBDIRS = $(SRC_PATH) $(EXAMPLES_PATH) 

all : subdirs
 
subdirs:
 @for i in $(SUBDIRS); do \
  echo    "===>$$i"; \
  (cd $$i && $(MAKE) -f $(MAKEFILE)) || exit 1; \
  echo    "<===$$i"; \
 done

clean:
 @for i in $(SUBDIRS); do \
  echo    "===>$$i"; \
  (cd $$i && $(MAKE) clean -f $(MAKEFILE)) || exit 1; \
  echo    "<===$$i"; \
 done

运行一下,由于examples目录下的Makefile还是空的,所以没有成功。

Tony : 有了前面的经验,相信完成examples目录下的两个Makefile对你来说不成问题。
Alex : I could not agree with you any more(Alex脸上满是笑容),我来完成它。
每个appdemoX下的Makefile(5)我设计成这样:
#
# Makefile for appdemoX
#
include ../../Make.properties

TARGET = appdemoX
SRC = ./src/appdemoX.cpp

all :
 $(CC) -o $(TARGET) $(SRC) $(CPPFLAGS) -L $(DISTDIR) -ldemo
 mv $(TARGET).exe ./bin

clean :
 rm -f ./src/*.o ./bin/$(TARGET).exe

而examples目录下的Makefile(4)的样子如下:
#
# Makefile for examples directory
#

include ../Make.properties

EXAMPLE1_PATH = appdemo1
EXAMPLE2_PATH = appdemo2

SUBDIRS = $(EXAMPLE1_PATH) $(EXAMPLE2_PATH) 

all : subdirs
 
subdirs:
 @for i in $(SUBDIRS); do \
  echo    "===>$$i"; \
  (cd $$i &&$(MAKE) -f $(MAKEFILE)) || exit 1; \
  echo    "<===$$i"; \
 done

clean:
 @for i in $(SUBDIRS); do \
  echo    "===>$$i"; \
  (cd $$i &&$(MAKE) clean -f $(MAKEFILE)) || exit 1; \
  echo    "<===$$i"; \
 done

Tony : 可以,不知不觉间,我们的工作已经接近尾声,剩下的工作就是细节了,包括编译器参数的细化等。
Alex : 在Makefile(1)中加上install,tar等目标,使用户得到有更多的功能。十分感谢你的指导。
Tony : 那晚上去原味斋吧,想烤鸭了^_^。

/*=============================================================
Note : Makefile常识
a) "=" vs ":="
例子:
C_OPTIONS = $(C_EXTRA_OPTION) -O2
C_EXTRA_OPTION = -g
cfoo: foo.c exam.c
    gcc $(C_OPTIONS) -o $@ $^
=>gcc -g -O2 -o cfoo foo.c exam.c

C_OPTIONS := $(C_EXTRA_OPTION) -O2
C_EXTRA_OPTION = -g
cfoo: foo.c exam.c
    gcc $(C_OPTIONS) -o $@ $^
=>gcc -O2 -o cfoo foo.c exam.c

大家发现不同了,Why? 使用“=”赋值的变量在使用时才被展开,并且每使用一次就会展开一次,其值每次展开的时候有可能是不同的,就如第一个C_OPTION由于在使用时展开,所以C_EXTAR_OPTION定义的位置不影响C_OPTION的值。而使用“:=”进行赋值的变量,则在赋值的时候就被展开,并且仅仅展开一次,从此以后其值将不会发生任何变化,就第二个C_OPTION由于定义时展开所以由于定义时看不到C_EXTAR_OPTION所以值为-02,而不是-g -02。

b) wildcard 函数
在 GNU Make 里有一个叫 'wildcard' 的函数,它有一个参数,功能是展开成一列所有符合由其参数描述的文件名,文件间以空格间隔。你可以像下面所示使用这个命令:SOURCES = $(wildcard *.cpp)   SOURCES = xx.cpp yy.cpp … zz.cpp
==============================================================*/




这里是Tony Bai的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:


以太币:


如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多