22 September 2005
This post may be outdated due to it was written on 2005. The links may be broken. The code may be not working anymore. Leave comments if needed.

NAME

Test::Tutorial - 如何写一个真正意义上的基本测试文件

DESCRIPTION

AHHHHHHH!!!! NOT TESTING! Anything but testing! Beat me, whip me, send me to Detroit, but don't make me write tests!

sob

Besides, I don't know how to write the damned things.

Is this you? Is writing tests right up there with writing documentation and having your fingernails pulled out? Did you open up a test and read

######## We start with some black magic

and decide that's quite enough for you?

It's ok. That's all gone now. We've done all the black magic for you. And here are the tricks...

Nuts and bolts of testing.

这可能是个最基础的测试程序:

#!/usr/bin/perl -w

print "1..1\n";

print 1 + 1 == 2 ? "ok 1\n" : "not ok 1\n";

由于 1+1 等于 2, 所以它输出:

1..1
ok 1

它的意思为: 1..1 表示“我们将要运行一个测试”,而 ok 1 表示第一个测试已通过。 这就是有关测试的所有魔法。你的基本测试单元就是 ok. 对于每一个你测试的东西,都有一个 ok 被输出。 这很简单。Test::Harness 运行你测试的结果,然后告诉你成功或是失败了(更多的期待下文)。

经常写这些 print 语句会让人很快就觉得乏味。幸运的是我们有 Test::Simple. 它有一个函数,ok().

#!/usr/bin/perl -w

use Test::Simple tests => 1;

ok( 1 + 1 == 2 );

它与上面的代码所做的是一样的。ok() 是 Perl 测试的脊椎,从此时起我们都用它来代替自己手写的部分。 如果 ok() 获得一个 true 值,测试通过。false, 则失败。

#!/usr/bin/perl -w

use Test::Simple tests => 2;
ok( 1 + 1 == 2 );
ok( 2 + 2 == 5 );

这会输出:

1..2
ok 1
not ok 2
#     Failed test (test.pl at line 5)
# Looks like you failed 1 tests of 2.

1..2 表示“我们要运行两个测试”。这个数字用以确定你的测试程序运行了所有的测试而没有中途 die 或跳过某些测试。 ok 1 表示“第一个测试通过”。 not ok 2 表示“第二个测试失败”。 Test::Simple 还能输出一些额外的注释。

It's not scary. Come, hold my hand. 我们将要对一个模块测试给出一个完整的例子。 在此,我们将测试一个日期库,Date::ICal. 它是 CPAN 的一部分,所以我们可以下载它然后跟着我来做。

从哪开始?

这是关于测试最困难的部分,我们从哪开始? 要测试一个完整的模块,人们通常会被这个任务外表看起来的巨大所压倒。 最好开始的位置就是开头。Date::ICal 是一个面对对象模块,这就意味着你可以从定义一个对象开始。所以我们先测试new().

#!/usr/bin/perl -w

use Test::Simple tests => 2;

use Date::!ICal;

my $ical = Date::ICal->new;         # create an object
ok( defined $ical );                # check that we got something
ok( $ical->isa('Date::ICal') );     # and it's the right class

运行它你就会得到:

1..2
ok 1
ok 2

祝贺你,你已经完成了你第一个有用的测试。

Names

这输出看起来不是很有描述性?当你只有 2 个测试时你能指出哪个是第 2 个,但如果你有 102 个呢?

我们可以给每一个测试一点描述性文字作为 ok() 的第二个参数。

use Test::Simple tests => 2;

ok( defined $ical,              'new() returned something' );
ok( $ical->isa('Date::ICal'),   "  and it's the right class" );

现在你会看到:

1..2
ok 1 - new() returned something
ok 2 -   and it's the right class

测试说明手册

最简单的创建一个象样的测试工具是只测试那些说明手册上它做的。 让我们从 Date::ICal/SYNOPSIS 中抽取点什么然后一个片断一个片断的测试它。

#!/usr/bin/perl -w

use Test::Simple tests => 8;

use Date::ICal;

$ical = Date::ICal->new( year => 1964, month => 10, day => 16, 
                         hour => 16, min => 12, sec => 47, 
                         tz => '0530' );

ok( defined $ical,            'new() returned something' );
ok( $ical->isa('Date::ICal'), "  and it's the right class" );
ok( $ical->sec   == 47,       '  sec()'   );
ok( $ical->min   == 12,       '  min()'   );    
ok( $ical->hour  == 16,       '  hour()'  );
ok( $ical->day   == 17,       '  day()'   );
ok( $ical->month == 10,       '  month()' );
ok( $ical->year  == 1964,     '  year()'  );

运行它你会得到:

1..8
ok 1 - new() returned something
ok 2 -   and it's the right class
ok 3 -   sec()
ok 4 -   min()
ok 5 -   hour()
not ok 6 -   day()
#     Failed test (- at line 16)
ok 7 -   month()
ok 8 -   year()
# Looks like you failed 1 tests of 8.

Whoops, 一个失败!Test::Simple 有用地指出了错误发生在哪一行,但没有更多的东西。 我们期望得到 17,但是没得到。那我们得到了什么呢?我不知道。 我们得在调试器中重新运行此测试或者得依靠 print 语句找出我们所得到的。

我们这里不这么做,这里我们用 Test::More 来替代 Test::Simple. Test::More 能做所有 Test::Simple 能做的,还有它所不能做的。 事实上, Test::More 与 Test::Simple 所使用的方法是/一模一样的/。 你可以简单的用 Test::More 替换 Test::Simple 所在的位置。 这也就是我们接下来要做的。

Test::More 比 Test::Simple 做得更多。在这一点上,一个最重要的区别是它在输出 "ok" 时提供了更多的信息。 虽然你几乎可以用一般的 ok() 来写任何一个测试,但是它不能告诉你到底哪来出错了。 相反的,我们将使用函数 is(), 它让我们声明我们期待与另一个是否一样:

#!/usr/bin/perl -w

use Test::More tests => 8;

use Date::ICal;

$ical = Date::ICal->new( year => 1964, month => 10, day => 16, 
                         hour => 16, min => 12, sec => 47, 
                         tz => '0530' );

ok( defined $ical,            'new() returned something' );
ok( $ical->isa('Date::ICal'), "  and it's the right class" );
is( $ical->sec,     47,       '  sec()'   );
is( $ical->min,     12,       '  min()'   );    
is( $ical->hour,    16,       '  hour()'  );
is( $ical->day,     17,       '  day()'   );
is( $ical->month,   10,       '  month()' );
is( $ical->year,    1964,     '  year()'  );

“$ical->sec 是否 47?”“$ical->min 是否 12?” 适当的使用 is(), 你会得到更多的信息:

1..8
ok 1 - new() returned something
ok 2 -   and it's the right class
ok 3 -   sec()
ok 4 -   min()
ok 5 -   hour()
not ok 6 -   day()
#     Failed test (- at line 16)
#          got: '16'
#     expected: '17'
ok 7 -   month()
ok 8 -   year()
# Looks like you failed 1 tests of 8.

这样我们知道了 $ical->day 返回了 16, 而我们期待的是 17。一个快速检查让我们发现代码运行是正常的,我们在写测试的时候犯了个小错误。所以我们将测试改一下:

is( $ical->day,     16,       '  day()'   );

这样所有的都运行通过了。

总之,当你在做这些“这个等于那个”类的测试时,使用 is() 会比较好。 它还能在数组上使用。测试总是在标量上下文下,所以你可以用这种方法来测试列表中包含了多少个元素。

is( @foo, 5, 'foo has 5 elements' );

有时候测试是错误的

上面我们有了一个重大的教训。代码是有 bugs 的,而测试也是代码。因此,测试也可能有 bugs 的。一个失败的测试意味着代码中有一个 bug, 但是小心考虑有可能是测试写错了。

另一方面,不要冲动地过早宣称某一测试是错误的,而原因只是你找不到 bug. 不要轻易的让某一测试无效,也不要让它作为你逃避工作的借口。

测试大量的值

接下来我们将要测试大量的数据,在许多不同的边缘情况下试图对代码做一些测试。测试它是否在 1970 年前也是可行的?2038 年后呢,或者 1904?10,000 年会不会出错?闰年正确吗?我们可以重复上述的代码,或者更好的建立一个 try/expect 循环。

use Test::More tests => 32;
use Date::ICal;

my %ICal_Dates = (
        # An ICal string     And the year, month, date
        #                    hour, minute and second we expect.
        '19971024T120000' =>    # from the docs.
                            [ 1997, 10, 24, 12,  0,  0 ],
        '20390123T232832' =>    # after the Unix epoch
                            [ 2039,  1, 23, 23, 28, 32 ],
        '19671225T000000' =>    # before the Unix epoch
                            [ 1967, 12, 25,  0,  0,  0 ],
        '18990505T232323' =>    # before the MacOS epoch
                            [ 1899,  5,  5, 23, 23, 23 ],
);


while( my($ical_str, $expect) = each %ICal_Dates ) {
    my $ical = Date::ICal->new( ical => $ical_str );

    ok( defined $ical,            "new(ical => '$ical_str')" );
    ok( $ical->isa('Date::ICal'), "  and it's the right class" );

    is( $ical->year,    $expect->[0],     '  year()'  );
    is( $ical->month,   $expect->[1],     '  month()' );
    is( $ical->day,     $expect->[2],     '  day()'   );
    is( $ical->hour,    $expect->[3],     '  hour()'  );
    is( $ical->min,     $expect->[4],     '  min()'   );    
    is( $ical->sec,     $expect->[5],     '  sec()'   );
}

所以现在我们想测试更多的数据时只需要将它们加入 %ICal_Dates 就可以了。 既然测试更多的数据是个很轻松的工作,那你将更倾向于测试比你所想的更多的数据。 唯一的问题是,每一次我们加入数据都需要更改 use Test::More tests => ##> 行。 这很快就让人厌烦。但我们有两种办法让生活更美好。

第一种,我们可以通过 plan() 函数动态地计算运行的项目。

use Test::More;
use Date::ICal;

my %ICal_Dates = (
    ...same as before...
);

# For each key in the hash we're running 8 tests.
plan tests => keys %ICal_Dates * 8;

或者更灵活的,我们使用 no_plan. 这意味着我们要运行很多测试,但不知道到底有多少。

use Test::More 'no_plan';   # instead of tests => 32

现在我们就能直接加进测试而不需要数出我们到底要运行多少项了。

Informative names

看一下下面这行:

ok( defined $ical,            "new(ical => '$ical_str')" );

我们已经对我们所要进行的测试加进了更多的细节, and the ICal string itself we're trying out to the name. 所以你得到类如下面的结果:

ok 25 - new(ical => '19971024T120000')
ok 26 -   and it's the right class
ok 27 -   year()
ok 28 -   month()
ok 29 -   day()
ok 30 -   hour()
ok 31 -   min()
ok 32 -   sec()

如果这里的某个测试失败了,那么我们就知道到底是哪一个,这样使跟踪问题变得简单。 所以请试着在测试名称中加入更多的调试信息。

描述我们在进行什么测试,这让调试一个失败的测试变得容易,不论是对于你或是任何运行你测试的人来说。

跳过测试

在已有的 Date::ICal 测试文件中找寻,我们会在 t/01sanity.t 中发现:

#!/usr/bin/perl -w

use Test::More tests => 7;
use Date::ICal;

# Make sure epoch time is being handled sanely.
my $t1 = Date::ICal->new( epoch => 0 );
is( $t1->epoch, 0,          "Epoch time of 0" );

# XXX This will only work on unix systems.
is( $t1->ical, '19700101Z', "  epoch to ical" );

is( $t1->year,  1970,       "  year()"  );
is( $t1->month, 1,          "  month()" );
is( $t1->day,   1,          "  day()"   );

# like the tests above, but starting with ical instead of epoch
my $t2 = Date::ICal->new( ical => '19700101Z' );
is( $t2->ical, '19700101Z', "Start of epoch in ICal notation" );

is( $t2->epoch, 0,          "  and back to ICal" );

新纪元的开头部分在大多数非 Unix 系统中是不一样的。 即使 Perl 对大部分差异进行了处理,但还是有某些地方仍然是不一样的。 MacPerl 是让我头疼的其中一种。我们知道上面的代码在 MacOS 中是不能运行的。 所以与其在每个测试前注释掉,我们还不如直接说这不能运行然后跳过这个测试。

use Test::More tests => 7;
use Date::ICal;

# Make sure epoch time is being handled sanely.
my $t1 = Date::ICal->new( epoch => 0 );
is( $t1->epoch, 0,          "Epoch time of 0" );

SKIP: {
    skip('epoch to ICal not working on MacOS', 6) 
        if $^O eq 'MacOS';

    is( $t1->ical, '19700101Z', "  epoch to ical" );

    is( $t1->year,  1970,       "  year()"  );
    is( $t1->month, 1,          "  month()" );
    is( $t1->day,   1,          "  day()"   );

    # like the tests above, but starting with ical instead of epoch
    my $t2 = Date::ICal->new( ical => '19700101Z' );
    is( $t2->ical, '19700101Z', "Start of epoch in ICal notation" );

    is( $t2->epoch, 0,          "  and back to ICal" );
}

这里有一点点小魔术。当我们不是运行在 MacOS 时,所有的测试项都会正常运行。但是在 MacOS 时, skip() 将跳过整个 SKIP 块的内容,它将不会被运行。还有,它将输出一些特定的东西告诉 Test::Harness 这些测试项已经被跳过。

1..7
ok 1 - Epoch time of 0
ok 2 # skip epoch to ICal not working on MacOS
ok 3 # skip epoch to ICal not working on MacOS
ok 4 # skip epoch to ICal not working on MacOS
ok 5 # skip epoch to ICal not working on MacOS
ok 6 # skip epoch to ICal not working on MacOS
ok 7 # skip epoch to ICal not working on MacOS

这就意味着你的测试项在 MacOS 中并非是失败的。这也意味着更少的 MacPerl 用户会发 emails 告诉你这些测试项失败了,而你知道这些是不可运行的。但是你需要对跳过的测试项加以注意。运行不正常和/不可运行/是不同的。它不是用于跳过真正的 bugs (我们马上就要讲了)。

这些所有的测试项将被完整的跳过。下面这样是可行的。

SKIP: {
    skip("I don't wanna die!");

    die, die, die, die, die;
}

Todo 测试项

翻译 Date::ICal 用户手册,我看到了:

ical

    $ical_string = $ical->ical;

Retrieves, or sets, the date on the object, using any
valid ICal date/time string.

“找回或设置”。Hmmm, 没有看见这个测试项:在 Date::ICal 测试套中用 ical() 来设置日期。所以我写了一个:

use Test::More tests => 1;
use Date::ICal;

my $ical = Date::ICal->new;
$ical->ical('20201231Z');
is( $ical->ical, '20201231Z',   'Setting via ical()' );

运行,然后得到:

1..1
not ok 1 - Setting via ical()
#     Failed test (- at line 6)
#          got: '20010814T233649Z'
#     expected: '20201231Z'
# Looks like you failed 1 tests of 1.

Whoops! 看起来这个是还没被实现的。让我们假设我们没有时间来修复它。 一般情况下,你可能直接将其注释掉或者在 todo 列表或其他地方加以说明。 这里,我们将直接通过将其放在 TODO 块中来声明“这个测试项将会失败”。

use Test::More tests => 1;

TODO: {
    local $TODO = 'ical($ical) not yet implemented';

    my $ical = Date::ICal->new;
    $ical->ical('20201231Z');

    is( $ical->ical, '20201231Z',   'Setting via ical()' );
}

现在当你运行的时候,会有一点点不同:

1..1
not ok 1 - Setting via ical() # TODO ical($ical) not yet implemented
#          got: '20010822T201551Z'
#     expected: '20201231Z'

Test::More 不再说“看起来你在 1 个测试中失败了 1 个”/"Looks like you failed 1 tests of 1"。 '# TODO' 告诉了 Test::Harness “这本就该是失败的”,然后它会将这个失败认为是正常的。 这样你就可以在你修复代码前写下测试。

如果一个 TODO 测试项通过了,Test::Harness 会报告 “未预料的成功”/"UNEXPECTEDLY SUCCEEDED". 当这个发生时,你只需要将 TODO 块和 local $TODO 删除掉。剩下来的就是一个真正的测试项了。

=head2 在污染 taint 模式下测试

Taint 污染模式是一件有趣的事。It's the globalest of all global features. 当你打开它时,它会影响你程序中/所有的/代码和/所有的/模块(和它们使用的模块)。 只要某一单独的代码片断不是 taint clean, 所有的事情都会被激发。在此观念下,保证你的模块在 taint 模式下运行是非常重要的。

设置在 taint 模式下运行代码是非常简单的。只要在 #! 行中增加一个 -T. Test::Harness 会读取此开关然后在你的测试中使用它。

#!/usr/bin/perl -Tw

...test normally here...

当 make test 时,它会运行在 taint 和 warnings 模式下。

AUTHORS

Michael G Schwern (schwern at pobox.com) and the perl-qa dancers!

本文由 Fayland (fayland at gmail.com) 翻译。

COPYRIGHT

Copyright 2001 by Michael G Schwern (schwern at pobox.com).

This documentation is free; you can redistribute it and/or modify it under the same terms as Perl itself.



blog comments powered by Disqus