Perl 脚本中单元测试自动化浅析
刘华婷,software engineer,IBM
Perl 单元测试框架的概述
随着敏捷开发模式的流行,如何快速高效地适应不确定或经常性变化的需求显得越来越重要。要做到这一点,需要在开发过程的各个阶段引入足够的测试。而其中单元测试则是保证代码质量的第一个重要关卡。
针对各种不同的语言,都有特有的单元测试框架。比如针对 Java 程序的单元测试框架JUnit,针对Python 程序的单元测试框架PyUnit,针对XML程序的单元测试框架XMLUnit 等。
目前,比较通用的 Perl 单元测试框架模块主要有 Test::Class 和 Test::Unit。
Test::Unit 类似于JUnit 框架,虽然它提供了通过子类的方式扩展测试类,但由于不基于 Test::Builder,无法用到 Test::Builder 系列测试模块的强大作用。Test::Class 同样支持通过创建子类、孙子类等重用测试类以及管理测试,同时,Test::Class 是基于 Test::Builder 模块创建的,因此可以使用任何 Test::Builder 系列的测试模块,如 Test::More、Test::Exception、Test::Differences、Test::Deep 等。这是 Test::Class 相对于 Test::Unit 的一大优势。
本文将结合具体实例,介绍如何创建基于 Test::Simple、Test::More 和 Test::Class 的 Perl 单元测试框架。
模块的安装
由于 ::Simple、Test::More和Test::Class 都不是标准模块,因此需要安装。可以用CPAN 方式在root 权限下安装。命令如下:
Perl -MCPAN -e ‘install Test::Class’
其他模块的安装方法类似。使用 CPAN 需要连接到网络,如果当前没有网络环境,可以根据事先下载好的模块的 readme 文件中的步骤安装相应模块。
Perl 单元测试框架
本节中,我们假设有一个模块 Hello.pm 需要测试,我们结合不同的 Perl 测试框架,讨论测试代码的写法,并从测试结果介绍他们的特点和作用。
Hello.pm 的源代码如下:
清单 1. 被测对象 Hello.pm 源代码
use strict;
use warnings;
package Hello;
$Hello::VERSION = '0.1';
sub hello {
my ($you)=@_;
return "Hello,$you!";
}
sub bye {
my ($you)=@_;
return "Goodbye,$you!";
}
1;
Test::Simple
我们先来介绍最简单、最基础的模块Test::Simple。之所以说这个模块是最简单最基础的,是因为这个模块只有一个 function ok()。语法如下:
Syntax: ok(Arg1,Arg2)
Arg1: 布尔表达式,如果这个表达式为真, 这个 testcase passed,否则 Failed;
Arg2: 这个参数是可选的,用来设置 testcase name。
因此,我们能很轻松地书写基于 Test::Simple 测试框架的测试代码。示例代码 test_simple.perl 如下:
清单 2. Test::Simple 示例代码
use strict;
use warnings;
use Test::Simple tests => 3;
use Hello; # What you're testing.
my $hellostr=Hello::hello('guys');
my $byestr=Hello::bye('guys');
ok($hellostr eq 'Hello,guys!','hello() works');
ok($byestr eq 'Goodbye,'bye() works');
my $helloworld=Hello::hello();
ok($hellostr eq 'Hello,world!','should be hello,world! by default');
需要特别说明的是,在写测试脚本之前,必须事先声明计划执行的 testcase 的个数,如:
use Test::Simple tests => 3;
我们在命令行中执行 perl test_simple.perl,观察程序的输出如下:
清单 3. Test::Simple 示例代码执行结果
C:\MySpace\workdir>perl test_simple.perl
1..3
ok 1 - hello() works
ok 2 - bye() works
Use of uninitialized value $you in concatenation (.) or string at Hello.pm line
9.
not ok 3 - should be hello,world! by default
# Failed test 'should be hello,world! by default'
# at hello.t line 12.
# Looks like you Failed 1 test of 3.
从测试结果中不难看出,第一个和第二个 testcase 成功通过测试,而第三个testcase 则失败了。
从字面意思上不难看出Test::More 比Test::Simple提供了更多更广泛的对testcase是否成功的支持。下面简单介绍其中的一些常用功能。
和 Test::Simple 一样,Test::More 同样需要事先申明需要测试的testcase的个数。比如:
use Test::More tests => 10;
然而,你可能在最初并不能预见到底需要测试多少个 testcase。为此 Test::More 提供了另外一种在最下方用 done_testing 的方式来达到这个目的。对应的代码如下:
use Test::More;
…… run your tests ……
done_testing($number_of_tests_run);
这时,你甚至可以用 skip_all 来跳过 testcase。
use Test::More skip_all => $skip_reason;
Test::More 中提供了许多使用的方法,表 1 中列举出了其中的一些。
表 1. 常用 Test::More 方法
方法 | 说明 | 用法 | ||
---|---|---|---|---|
ok | 判断 testcase ok | ok($got op $expected,$test_name); | ||
is/isnt | 字符串比较 | is($got,$expected,204); border-top-style:solid; border-top-width:1px; padding-top:8px; padding-right:5px; padding-bottom:8px; padding-left:5px; vertical-align:top"> | isnt($got,$test_name); | |
like/unlike | 正则表达式比较 | like( $got,qr/expected/,$test_name ); | ||
nlike( $got,204); border-top-style:solid; border-top-width:1px; padding-top:8px; padding-right:5px; padding-bottom:8px; padding-left:5px; vertical-align:top; text-align:left"> cmp_ok | 可以指定操作符地比较 | cmp_ok($got,$op,204); border-top-style:solid; border-top-width:1px; padding-top:8px; padding-right:5px; padding-bottom:8px; padding-left:5px; vertical-align:top; text-align:left"> can_ok | 被测模块或对象的方法 | can_ok($module,@methods) |
can_ok($object,@methods) | ||||
isa_ok | 对象是否被定义或对象的实例变量确实是已定义的引用 | isa_ok($object,$class,$object_name); | ||
isa_ok($subclass,204); border-top-style:solid; border-top-width:1px; padding-top:8px; padding-right:5px; padding-bottom:8px; padding-left:5px; vertical-align:top; text-align:left"> isa_ok($ref,$type,$ref_name); | ||||
subtest | 测试子集 | subtest $name=>\&code; | ||
pass/fail | 直接给出通过 / 不通过 | pass($test_name); | ||
fail($test_name); | ||||
use_ok | 测试加载模块并导入相应符号是否成功 | BEGIN \{use_ok($module);} | ||
BEGIN \{use_ok($module,@imports);} | ||||
is_deeply | 复杂数据结构的比较 | is_deeply($got,$test_name); | ||
new_ok | 判断创建的对象是否 ok | my $obj=new_ok($class); | ||
my $obj=new_ok($class=>@args); | ||||
my $obj=new_ok($class=>@args,$object_name); |
这里,我们着重介绍其中的几个。 is(Arg1,Arg2,Arg3) 类似于 ok(),用 eq 操作符比较 Arg1 和 Arg2 的值来决定 testcase 成功还是失败。Arg3 是指 testcase 的名字。 like( Arg1,Arg3 ) Arg2 是一个正则表达式,比较 Arg1 是否 匹配 Arg2 正则表达式。Arg3 是指 testcase 的名字。 cmp_ok( Arg1,Arg3,Arg4 ); cmp_ok() 允许用任何二元操作符(Arg2)比较 Arg1,Arg3. 同样,Arg4 是指 testcase 的名字。另外 cmp_ok() 有个好处,如果 testcase Failed,结果中会报告 Arg1 和 Arg2 在运行中的实际值。 can_ok($module,@methods); can_ok($object,@methods); can_ok() 判断模块 $module 或对象 $object 能否调用方法 @methods。 更多方法可以参考 CPAN 上关于 Test::More 的更多的介绍。 基于这些函数,我们能非常方便的设计和实现基于 Test::More 的 testcase。示例代码 test_more.perl 如下: 清单 4. Test::More 示例代码 use strict; use warnings; use Test::More tests => 3; use Hello; # What you're testing. my $hellostr=Hello::hello('guys'); my $byestr=Hello::bye('guys'); is($hellostr,'Hello,'hello() works'); like($byestr,"/Goodbye/",'bye() works'); cmp_ok($hellostr,'eq','bye() works'); can_ok('Hello',qw(hello bye)); 在命令行下运行 perl test_more.perl 后,我们可以看到程序的输出如下: 清单 5. Test::More 示例代码执行结果 C:\Perl\test> perl more.perl 1..3 ok 1 - hello() works ok 2 - bye() works ok 3 - bye() works ok 4 - Hello->can(...) # Looks like you planned 3 tests but ran 4. cmp_ok($hellostr,qw(hello bye)); Test::Class 的介绍 定义一个测试类,只需要编写一个从 Test::Class 继承的子类,申明如下: use base qw(Test::Class); 由于 Test::Class 本身没有提供测试函数,而是使用 Test::More 之类的其他测试框架的方法,因此需要申明 Test::More 模块 : use Test::More; Test::Class 的常用方法包括: Test 方法 sub method_name:Test \{...}; sub method_name:Test(N)\{...}; 列表项中可以包含代码清单,表格和图片(例如一系列图片以列表项的形式组织到一起); N: 代表函数内测试判断执行数量,相当于执行 case 数目。默认代表只执行 1 个 case. 如果你无法判断执行 case 数目,如循环执行 case。那么,可以使用 sub method_name:Test(no_plan) \{...} 或 sub method_name:Tests\{...}。 Setup 和 teardown 方法 setup 和 teardown 分别在每个普通测试方法之前和之后调用。 Setup 用法 sub method_name:Test(setup) \{...}; sub method_name:Test(setup=>N)\{...}; teardown 用法 sub method_name : Test(teardown) \{ ... }; sub method_name : Test(teardown => N) \{ ... }; startup 和 shutdown 方法 startup 和 shutdown 方法用法和 Setup,teardown 方法类似,区别在与 Startup 和 shutdown 是在所有测试方法执行之前和之后调用。 Runttests 方法 通过调用 runtests() 方法,执行所有从 Test::Class 派生的子类中定义的测试 case,用法: Test::Class->runtests(); 下面的例子是基于 Test::Class 的测试代码 test_class.perl。 清单 6. Test::Class 示例代码 use strict; use warnings; use Hello; # What you're testing. use Test::More; use base qw(Test::Class); my $hellostr=Hello::hello('guys'); my $byestr=Hello::bye('guys'); sub initial : Test(setup) { print "Begin One Test...\n"; } sub end : Test(teardown) { print "End One Test...\n"; } sub test_hello : Test(1) { is($hellostr,'hello() works'); } sub test_bye : Test(1) { like($byestr,'bye() works'); } Test::Class->runtests(); 在命令行中执行 perl test_class.perl 后的测试结果如下: 清单 7. Test::Class 示例代码执行结果 C:\Perl\test> perl test_class.perl Begin One Test... 1..2 ok 1 - bye() works End One Test... Begin One Test... ok 2 - hello() works End One Test... 回页首 应用实例 有了之前对于几个常用 Perl 单元测试框架的介绍,下面我们给出一个具体的实例,来测试一个 CPAN 中的一个 module File::Util。部分单元测试的代码如下。 清单 8. 应用实例代码 #!/usr/bin/perl -w use strict; use File::Util; use Test::More; use base qw(Test::Class); my $file; sub init : Test(startup){ print "####################################################\n"; print "This script is used to test some subs in File::Util.\n"; $file = File::Util->new(); } sub shutdown : Test(shutdown){ print "Finished all testcases.\n"; print "####################################################\n"; } sub initial : Test(setup) { print "----------------------------------------------------\n"; print "Begin One Test...\n"; } sub end : Test(teardown) { print "End One Test...\n"; print "----------------------------------------------------\n"; } sub test_methods : Test(1) { can_ok('File::Util',qw(existent line_count list_dir)); } sub test_existent_true : Test(1) { open(FILE,">test.txt"); print FILE "This is a test."; close FILE; cmp_ok($file -> existent('test.txt'),"==",1,'test_existent_file_exists'); unlink "test.txt"; is($file -> existent('test.txt'),undef,'test_existent_file_not_exists'); } sub test_line_count : Test(1) { open(FILE,">test.txt"); print FILE "This is a test.\n"; close FILE; cmp_ok($file -> line_count('test.txt'),'test_line_count = 1'); open(FILE,">test.txt"); print FILE ""; close FILE; cmp_ok($file -> line_count('test.txt'),'test_line_count = 0'); } Test::Class->runtests(); 程序中设计了三个 testcase,分别用于测试模块中的方法的命名空间可见性、existent 方法和 line_count 方法。测试的结果如下: 清单 9. 应用实例代码执行结果 #################################################### This script is used to test some subs in File::Util. ---------------------------------------------------- Begin One Test... 1..3 ok 1 - test_existent_file_exists ok 2 - test_existent_file_not_exists # expected 1 test(s) in main::test_existent_true,2 completed End One Test... ---------------------------------------------------- ---------------------------------------------------- Begin One Test... ok 3 - test_line_count = 1 ok 4 - test_line_count = 0 # expected 1 test(s) in main::test_line_count,2 completed End One Test... ---------------------------------------------------- ---------------------------------------------------- Begin One Test... ok 5 - File::Util->can(...) End One Test... ---------------------------------------------------- Finished all testcases. #################################################### # Looks like you planned 3 tests but ran 5. 回页首 结束语 随着敏捷开发模式的流行,单元测试的自动化也显得尤其重要。本文介绍了 CPAN 上单元测试相关的几个模块 Test::Simple,Test::more 和 Test::class,并且结合实例具体讲解实现方法。一旦灵活掌握了这些模块使用方法将有助于提高软件开发和测试的效率。同时,CPAN 中还有许多使用的模块,灵活应用这些模块很很好的帮助我们高效和高质量的进行软件开发。 参考资料 学习 访问 CPAN 中 Test::Simple 模块的在线文档,进行更加深入的研究。 访问 CPAN 中 Test::More 模块的在线文档,进行更加深入的研究。 访问 CPAN 中 Test::Class 模块的在线文档,进行更加深入的研究。 访问 CPAN 中 File::Util 模块的在线文档,以便进一步了解。 随时关注 developerWorks 技术活动和网络广播。 访问 developerWorks Open source 专区获得丰富的 how-to 信息、工具和项目更新以及最受欢迎的文章和教程,帮助您用开放源码技术进行开发,并将它们与 IBM 产品结合使用。