Perl 脚本中单元测试自动化浅析

随着敏捷开发模式的流行,单元测试的自动化也显得尤其重要。本文介绍了 CPAN 上单元测试相关的几个模块 Test::Simple,Test::more 和 Test::class,并且结合实例具体讲解了基于 Perl 脚本的单元测试自动化的实现方法。

刘 华婷, software engineer, IBM

刘华婷是 IBM CSTL 的一名软件开发工程师,主要从事 Director Build 方面的工作,对 Perl 和单元测试自动化有浓厚的兴趣。



2012 年 3 月 22 日

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, guys!', '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::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,$test_name);
isnt($got,$expected,$test_name);
like/unlike 正则表达式比较 like( $got, qr/expected/, $test_name );
nlike( $got, qr/expected/, $test_name );
cmp_ok 可以指定操作符地比较 cmp_ok($got,$op,$expected,$test_name);
can_ok 被测模块或对象的方法 can_ok($module,@methods)
can_ok($object,@methods)
isa_ok 对象是否被定义或对象的实例变量确实是已定义的引用 isa_ok($object,$class,$object_name);
isa_ok($subclass,$class,$object_name);
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,$expected,$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, Arg2, Arg3 )

    Arg2 是一个正则表达式 , 比较 Arg1 是否 匹配 Arg2 正则表达式。Arg3 是指 testcase 的名字。

  • cmp_ok( Arg1, Arg2, 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, guys!', 'hello() works'); 
 like($byestr, "/Goodbye/", 'bye() works'); 
 cmp_ok($hellostr, 'eq', 'Hello, guys!', '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, 'eq', 'Hello, guys!', 'bye() works'); 
 can_ok('Hello', 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, guys!', 'hello() works'); 
 } 
 sub test_bye : Test(1) { 
	 like($byestr, "/Goodbye/", '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'), "==" ,1, 'test_line_count = 1'); 
	 open(FILE, ">test.txt"); 
	 print FILE ""; 
	 close FILE; 
	 cmp_ok($file -> line_count('test.txt'), "==" ,0, '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 中还有许多使用的模块,灵活应用这些模块很很好的帮助我们高效和高质量的进行软件开发。

参考资料

学习

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=806575
ArticleTitle=Perl 脚本中单元测试自动化浅析
publish-date=03222012