Perl单元测试
1 测试内容和常用模块 CPAN上有很多成熟的模块可以拿来帮助我们对perl脚本做单元测试,本文整理了它们的用法。 · perl模块推荐
Devel::Cover是一个代码覆盖度测试的很棒的模块,它能自动分析并且生成一份详细的报告,而且可以生成html版本的,方便阅读 2.1模块安装 1、root帐号下,用CPAN方式安装,以解决模块依赖问题。(命令行输入perl -MCPAN -e 'install Devel::Cover') 2、安装完毕后,运行cover -v, 打开cover脚本(which cover),看看第一行的perl路径是否是系统perl的路径(which perl),不是的话,替换成后者。这样做的一个原因,是我们有可能会更新了Perl版本(譬如从5.8版本升级到5.12版本),而cover程序却可能依然调用老版本的Perl,这样可能会造成版本不一致带来的bug --事实上在我的机子上就发生了这样的问题。 从CPAN上下载Devel::Cover安装包,解压缩后,将其中的cover、cpancover、gco2perl三个文件修改首行的perl路径,并放到perl路径下去(或任何一个PATH环境变量的目录) 2.2从一个例子开始 如下是一个名叫wiki.pl的perl脚本,它定义了GetDivInt函数和echo函数,并且两次调用GetDivInt函数,最后脚本退出 use strict; use warnings; sub GetDivInt { my ($a,$b) = @_; return 0 if($a== 0 || $b == 0); my $r = $a/$b; if($r < 0.0001){ $r = 0; } $r = $1 if($r =~ /^(d+)./); return $r; } ? sub echo { print "@_n"; } ? print GetDivInt(3,4); print GetDivInt(0,2); 运行如下命令 $ cover -delete $ perl -MDevel::Cover wiki.pl $ cover 2.2.1查看覆盖率报告 运行完cover后,会在当前目录下生成一份包括HTML格式的表格化的总结报告。 2.2.2分析覆盖率报告 覆盖测试有哪些类型? 语句覆盖 看是否有一项测试执行了某一条语句。对于给定的语句 $flag = 1; 任何测试项目在运行时执行了这条语句的话,就认为已经覆盖到了该语句。 ? 分支覆盖 跟踪是否有测试项目执行了分支语句的各个部分。对于给定的代码 print "True!" if $flag; ,该语句必须被执行两次(分别当$flag为真和假),才算打到百分百的分支覆盖率。 ? 条件覆盖 考察逻辑表达式的各种可能性。对于这样的短路表达式 $a = $x || $y; ,需要三种不同的测试来覆盖(00、01、1x)。 ? 子程序覆盖 检查测试项目是否至少运行了子程序的一部分。 · 覆盖率报告
· 分支覆盖率报告
2.2.3我们刚才做了什么? 我们刚才运行了三条语句,然后就得到了覆盖率报告,下面讲一下这三条语句的含义。 · cover -delete · perl -MDevel::Cover wiki.pl · cover 以上是对一个脚本进行统计,除此外,还有以下的几种情况。 · 对一个未安装模块的测试 cover -test 或 cover -delete HARNESS_PERL_SWITCHES=-MDevel::Cover make test cover · 对一个未安装模块的测试,如果它是用Module::Build模块来安装的 ./Build testcover · 如果这些模块不使用t/*.t的测试架构 PERL5OPT=-MDevel::Cover make test 更具体的内容,可以上CPAN搜索Devel::Cover进行查看 2.2.4缺陷 Devel::Cover模块没有重大的功能缺陷或bug,除了官方公布的缺陷外,我在使用过程中还发现它目前并不支持对多线程的脚本的覆盖率测试。 3性能检测 性能检测一方面是查看脚本运行时CPU、内存等使用情况,另一方面是检查脚本的运行效率,了解脚本速度瓶颈在哪,继而解决瓶颈问题。前者可以用top、vmstat等系统命令观察,后者才是下面讨论的内容。 3.1模块安装 Benchmark和Devel::DProf(包括dprofpp程序)都是默认安装的模块,Devel::SmallProf需要人工安装,依然建议CPAN方式安装。 3.2一个例子 以下名为test.pl的Perl脚本定义了三个函数,功能都是在一个或两个有序数组中查找一个数字是否存在。(为分析简单,没有use strict;use warnings;) @a = (1..999999); @b = (22222..2222211); ? sub in_a { $num = $_[0]; @arr = @{$_[1]}; foreach(@arr){ return 1 if $_ == $num; } return 0; } ? sub in_b { $num = $_[0]; @arr = @{$_[1]}; ? ($ta,$tb) = (0,scalar @arr - 1); while($ta<$tb) { $mid = int( ($tb+$ta) / 2 ); return 1 if $arr[$mid] == $num; if($arr[$mid] > $num){ $tb = $mid; }else{ $ta = $mid; } } return 0; } ? sub in_both { return 0 if in_b($_[0],$_[1]) == 0; return 0 if in_b($_[0],$_[2]) == 0; return 1; } ? in_a(999998,@a); in_b(999998,@a); in_both(999998,@a,@b); 获取运行时间报告 $ perl -d:DProf test.pl $ dprofpp 获取到的输出如下: Total Elapsed Time = 0.969994 Seconds User+System Time = 0.969994 Seconds Exclusive Times %Time ExclSec CumulS #Calls sec/call Csec/c Name 35.0 0.340 0.340 3 0.1133 0.1133 main::in_b 18.5 0.180 0.180 1 0.1800 0.1800 main::in_a 0.00 - -0.000 1 - - main::BEGIN 0.00 - 0.240 1 - 0.2400 main::in_both 第一行表示程序的运行时间,第二行显示了代码执行时间和系统调用时间的。 %Time表示这个子进程调用花的时间的百分比;ExclSec表示在这个子进程上花的时间(单位为秒,不包括它所调用子进程的运行时间);CumulS 表示在这个子进程上花的时间(单位为秒,包括它所调用子进程的运行时间);#Calls表示调用该子进程的次数;sec/call表示每次调用该子过程花 费的时间(单位为秒,不包括它所调用子进程的运行时间);Csec/c,平均每次调用该子过程花费的时间(单位为秒,包括它所调用子进程的运行时间)。 从报告中,我们可以看到,用二分查找算法的in_b函数,运行时间比简单遍历的in_a函数节省了30%多。 除了前面讲到的-I参数,dprofpp还有很多其它参数,给我们分析运行时间提供了很多遍历,具体可以参考CPAN上的帮助文档(其实你直接perldoc dprofpp也行)。 4内存相关 Perl脚本的内存问题,一般都不是问题,这是因为我们写的perl脚本都比较短小,很少涉及大内存的操作,并且不会长时间运行从而造成内存泄漏。
1、不要将大文件一次性读入。譬如@arr = < FH >; 或undef $/; $str = < FH >; 。这是因为Perl的哈希和数组结构在记录数值的同时,还需要记录大量其它标志信息,因此内存开销非常大。如果需要对大文件进行原地修改,并且非得一次性读入文件才行的话,推荐使用Tie::File模块,需要注意的是,对于单条记录数据长度很短的文件,该模块在处理时存放偏移量的内存开销非常大,因此并不适用于处理大量短小记录的文件。 2、速度不是瓶颈的话,少用多线程。多线程除了耗内存外,还是不安全的,perl的多线程机制由于是后期版本硬加入的,一直以来都没有完全解决它的一些基本的设计缺陷。 ? 其它,推荐两个模块,Devel::Memalyzer和Devel::Size。 前者是一个perl程序内存使用情况的分析框架,后者很方便用来测试一个数据结构的占用内存。Devel::Memalyzer需要下载安装(注意将安装 包中script目录下的memalyzer.pl、memalyzer-combine.pl更改perl执行路径后放到PATH目录 下),Devel::Size默认随Perl一起发布。 5单元测试框架 Test::Class和Test::Unit是两个最常用的perl脚本单元测试框架的模块。 5.1模块安装 Test::Class不是标准模块,因此需要我们自己进行安装。方便起见,依然推荐root帐号下,用CPAN方式安装。此处不再赘述。 5.2一个例子 5.2.1 待测库 如下是一个名叫Game.pm的库,它定义了reversHash和getPlayers两个方法。 package Game; use strict; use warnings; sub reversHash { my %in = %{$_[0]}; my %out; foreach my $key (keys %in){ foreach(@{$in{$key}}){ push @{$out{$_}},$key; } } return %out; } ? sub getPlayers { my ($player_hash_ref,$game) = @_; my $game_hash_ref = Game::reversHash($player_hash_ref); if(exists $game_hash_ref->{$game}){ return @{$game_hash_ref->{$game}}; }else{ return ""; } } ? 1; 5.2.2 测试程序 针对这个库,我们利用Test::Class来编写测试程序test.pl,如下: use Game; use Test::More; use base qw(Test::Class); ? my %players; ? sub initial : Test(setup) { %players = ( '小A' => ['拔河','嗒嗒球'], '小B' => ['嗒嗒球','绑腿跑'], '小C' => ['拔河','嗒嗒球'] ); print "Begin One Test...n"; } ? sub end : Test(teardown) { print "End One Test...n"; } ? sub test_reverse : Test(1) { my $games_ref = Game::reversHash(%players); my %games = ( '拔河' => ['小A','小C'], '嗒嗒球' => ['小A','小B', '绑腿跑' => ['小B'] ); is_deeply($games_ref,%games,"reverse_Hash test"); } ? sub test_getPlayers : Test(2) { my @players = Game::getPlayers(%players,'绑腿跑'); is_deeply(['小B'],@players,"getPlayers test: match set"); ok( Game::getPlayers(%players,'篮球') eq "","getPlayers test: empty set"); } ? Test::Class->runtests(); 然后我们运行一下 $ perl test.pl Begin One Test... 1..3 ok 1 - getPlayers test: match set ok 2 - getPlayers test: empty set End One Test... Begin One Test... not ok 3 - reverse_Hash test # Failed test 'reverse_Hash test' # at test.pl line 23. # (in main->test_reverse) # Structures begin differing at: # $got->{嗒嗒球}[0] = '小C' # $expected->{嗒嗒球}[0] = '小A' End One Test... # Looks like you failed 1 test of 3. 5.2.3测试结果分析 由于我们的测试程序派生于Test::Class,因此我们 use base qw(Test::Class); (关于base的问题,可以查看base模块说明文档);由于我们的测试程序需要用到Test::More的is_deeply、ok方法,因此我们 use Test::More; 。 Test::Class非常聪明,它能跟踪任何从它派生出来的子类的信息,通过调用runtests()方法,所有测试case得到执行。 Test(setup) 和 Test(teardown) 是一套测试夹具,Test::Class会在每个普通测试方法之前调用 initial ,并在结束之后调用 end 。因为我们的测试程序定义了两个测试方法, test_reverse 和 test_getPlayers ,因此我们在测试结果输出中看到打印了两遍“Begin One Test..."、"End One Test..."。 除了setup和teardown两种方法外,还有startup和shutdown两种方法,它们对单个测试文件来说,只在启动第一个测试方法和结束最后一个测试方法时被运行,而不是每个测试方法都运行。你可以看到,其实我们这个测试程序,使用starup和shutdown更为合适,因为我们的两个测 试方法共用同一份初始化数据,并且,shutdown在这里还是完全可以省略的,因为它实际上没干什么必要的活。 因为 test_getPlayers 中一共进行了两个测试( is_deep 和 ok ),因此我们函数定义时,用了 Test(2) ,同理对于 test_reverse 。 is_deeply 是Test::More模块中用来比较复杂数据结构的方法(除此外,Test::Builder系列还有很多其它的好用的方法--要不我怎么说比之Test::Unit我更喜欢Test::Class呢 )。那么为什么我们的 test_reverse 里面的 is_deeply 会测试失败呢?原因在于Game.pm中的reversHash函数,是用 keys %in 获取hash的key的,而我们知道用这种办法获取出来的key是乱序的,并不保证跟我们定义hash时key的先后顺序相同。可以用一行代码做个实验: $ perl -e '%hash = qw(小A 1小B 2小C 3);print keys %hash' 小C小A小B 可以看到,输出key时已经乱序了。 5.2.4更进一步 前面我们看到Game.pm的reversHash函数处理后的数组值,都是乱序排列的,如果作为研发的某甲决定在Game.pm的基础上扩展一个子类,在这个子类里更改reversHash函数的行为,使得原先乱序排列的结果变成按字典排序,某甲决定命名该子类名字为Game::Sort.pm,因此他在Game.pm文件的同级目录上,创建了一个Game目录,并在Game目录中编写了Sort.pm库,代码如下: package Game::Sort; ? use strict; use warnings; ? use base 'Game'; ? sub reversHash { my %in = %{$_[0]}; my %out; foreach my $key (sort keys %in){ foreach(@{$in{$key}}){ push @{$out{$_}},$key; } } return %out; } ? 1; 可以看到,Game::Sort.pm修改了Game.pm的reversHash函数,把原先的 keys %in 改成了 sort keys %in ,从而实现字典排序。 1.把test.pl改成Game::Test.pm模块,除了把test.pl改名Test.pm,并且扔到Game目录下以外,我们还得把代码修改如下: package Game::Test; ? use Game; use Test::More; use base qw(Test::Class); ? my %players; ? sub initial : Test(starup) { %players = ( '小A' => ['拔河','嗒嗒球'] ); print "Begin Game::Test...n"; } ? sub end : Test(shutdown) { print "End Game::Test...n"; } ? sub test_reverse : Test(1) { my $games_ref = Game::reversHash(%players); my %games = ( '拔河' => ['小C','小A'], '嗒嗒球' => ['小C','小A','小B'],"getPlayers test: empty set"); } ? 1; 在这里,我们除了把整个文件package成Game::Test包以外,还去掉了runtests(),因为我们之后会把测试执行命令写到一个单独的脚本中去。并且修改setup、teardown为starup、shutdown。当然,由于Game.pm的reversHash函数的乱序问题,我们 也相应修改了test_reverese测试方法,以使得测试能成功。 2.新建一个Game::Sort::Test.pm模块,继承Game::Test类,为此我们在Game目录下新建Sort目录,并在Sort目录下编写Test.pm,代码如下: package Game::Sort::Test; ? use base 'Game::Test'; ? use Game::Sort; use Test::More; use strict; use warnings; ? my %players; ? sub initial : Test(starup) { %players = ( '小A' => ['拔河','嗒嗒球'] ); print "Begin Game::Sort::Test...n"; } ? sub end : Test(shutdown) { print "End Game::Sort::Test...n"; } ? sub test_reverse : Test(1) { my $games_ref = Game::Sort::reversHash(%players); my %games = ( '拔河' => ['小A',"reverse_Hash test"); } ? 1; 在这里,我们重定义了test_reverse函数,以期对Game::Sort的reversHash函数进行测试。 3.将测试执行语句写入a.t use Game::Sort::Test; use Game::Test; ? Test::Class->runtests(); 这里的runtests()方法,会将父类和子类的测试case一并执行。 4.执行测试脚本 $ prove a.t a.t .. ok All tests successful. Files=1,Tests=6,0 wallclock secs ( 0.02 usr 0.00 sys + 0.05 cusr 0.00 csys = 0.07 CPU) Result: PASS 因为我们不关心执行过程的细节,只关注测试结果报告,因此这里用prove,这是一个执行测试的命令,并且默认不显示测试细节(当然,你可以用-v参数打开测试细节)。关于prove的细节,你可以查看prove文档。 ? 5.3总结 从上面的例子,我们可以看到用Test::Class来做单元测试的强大与便利,虽然后面"更进一步"的类继承的示例看上去稍复杂了一点,但实际情况是我 们用前面的test.pl就已经能满足绝大部分的单元测试需求了。Test::Class还可以设置SKIP和TODO标签,以跳过或临时跳过某个类的测 试,具体可以查看Test::Class文档。
《Perl Testing程序高手秘笈》 ? (全文完)
【本文转自
百度测试技术空间】
http://hi.baidu.com/baiduqa/blog/item/13d08d7d242d2e1a28388a7a.html
【
关注百度技术沙龙】
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |