内容

名称

Test::Tutorial - 关于编写基础测试的教程

描述

啊!不要测试!除了测试什么都可以!打我,鞭打我,把我送到底特律,但不要让我写测试!

*啜泣*

此外,我不知道如何编写这些该死的东西。

这是你吗?编写测试是否与编写文档和拔掉指甲一样痛苦?你是否打开了一个测试并读到

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

然后决定这对你来说已经足够了?

没关系。现在这一切都过去了。我们已经为你完成了所有黑魔法。以下是一些技巧...

测试的基本原理。

这是最基本的测试程序。

#!/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 "我将运行一个测试。" [1] ok 1 "第一个测试通过了"。这就是测试的全部魔力。测试的基本单位是 *ok*。对于你测试的每一项,都会打印一个 ok。很简单。 Test::Harness 会解释你的测试结果,以确定你成功还是失败(稍后会详细介绍)。

不断地编写这些打印语句会变得很乏味。幸运的是,有 Test::Simple。它有一个函数 ok()

#!/usr/bin/perl -w

use Test::Simple tests => 1;

ok( 1 + 1 == 2 );

它与之前的代码做同样的事情。ok() 是 Perl 测试的支柱,我们将使用它而不是从这里开始自己编写。如果 ok() 得到一个真值,则测试通过。如果得到假值,则测试失败。

#!/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 "我将运行两个测试。" 这个数字是一个 *计划*。它有助于确保你的测试程序一直运行到最后,并且没有死掉或跳过一些测试。ok 1 "第一个测试通过了"。not ok 2 "第二个测试失败了"。Test::Simple 会有帮助地打印出一些关于你的测试的额外评论。

这并不吓人。来,握住我的手。我们将举一个测试模块的例子。在我们的例子中,我们将测试一个日期库,Date::ICal。它在 CPAN 上,所以下载一个副本并跟着做。[2]

从哪里开始?

这是测试中最难的部分,你从哪里开始?人们经常会被测试整个模块的任务的巨大性所压倒。最好的起点是开头。 Date::ICal 是一个面向对象的模块,这意味着你从创建一个对象开始。测试 new()

#!/usr/bin/perl -w

# assume these two lines are in all subsequent examples
use strict;
use warnings;

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

恭喜!你写了你的第一个有用的测试。

名称

这个输出并不太具有描述性,是吗?当你有两个测试时,你可以弄清楚哪个是 #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

测试手册

构建一个不错的测试套件的最简单方法是测试手册中所说的内容。[3] 让我们从 "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.

哎呀,失败了![4] Test::Simple 有帮助地让我们知道失败发生在哪个行,但除此之外没有更多信息。我们应该得到 17,但我们没有得到。我们得到了什么?不知道。你可以重新运行调试器中的测试,或者添加一些打印语句来找出原因。

相反,从 Test::Simple 切换到 Test::MoreTest::MoreTest::Simple 做的所有事情,以及更多!事实上,Test::More 做事情的方式与 Test::Simple 完全 一样。你可以直接将 Test::Simple 替换掉,并用 Test::More 代替它。这正是我们接下来要做的。

Test::MoreTest::Simple 做得更多。目前最重要的区别是它提供了更具信息性的方法来表达 "ok"。虽然你可以用一个通用的 ok() 编写几乎任何测试,但它无法告诉你哪里出了问题。is() 函数让我们可以声明某件事应该与另一件事相同

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()。它甚至适用于数组。测试始终处于标量上下文中,因此您可以通过这种方式测试数组中包含多少个元素。[5]

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

有时测试是错误的

这带来了一条非常重要的教训。代码有错误。测试是代码。因此,测试也有错误。测试失败可能意味着代码中存在错误,但不要排除测试本身错误的可能性。

另一方面,不要因为难以找到错误而过早地宣布测试不正确。使测试无效不是一件轻而易举的事,不要将其作为逃避工作的借口。

测试大量值

我们将要测试很多日期,试图用很多不同的边缘情况来欺骗代码。它在 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, day
        #                    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;

...and then your tests...

为了更加灵活,请使用 done_testing。这意味着我们只是运行一些测试,不知道有多少。[6]

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

... # tests here

done_testing();   # reached the end safely

如果您没有指定计划,Test::More 期望在您的程序退出之前看到 done_testing()。如果您忘记了,它会向您发出警告。您可以为 done_testing() 提供一个可选的预期运行测试数量,如果实际运行的测试数量不同,Test::More 会向您发出另一种警告。

信息丰富的名称

看一下这一行

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

我们在测试名称中添加了更多关于我们正在测试的内容和我们正在尝试的 ICal 字符串本身的详细信息。所以你会得到这样的结果

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 [7] 中发现了这个

#!/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 操作系统上的纪元开始时间是不同的 [8]。尽管 Perl 在大多数情况下消除了这些差异,但某些端口的实现方式不同。MacPerl 就是其中之一。[9] 与其在测试中添加注释并希望有人在调试失败时阅读测试,不如我们明确地说它永远不会工作并跳过测试。

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 Mac OS', 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 块中的所有内容都被跳过。它永远不会运行。相反,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 用户的关于失败测试的电子邮件会减少,因为你知道这些测试永远不会工作。你必须小心使用跳过测试。这些是针对那些不工作并且 *永远不会工作* 的测试。它不适用于跳过真正的错误(我们稍后会讲到这一点)。

测试完全被跳过。[10] 这将起作用。

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

    die, die, die, die, die;
}

待办事项测试

在浏览 Date::ICal 手册页时,我发现了这个

ical

    $ical_string = $ical->ical;

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

"检索或设置"。嗯,我在 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.

糟糕!看起来它还没有实现。假设你没有时间修复它。[11] 通常,你只需注释掉测试并在待办事项列表中添加一个注释。相反,通过将它包装在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 个”。那个“# TODO”告诉 Test::Harness “这应该失败”,它将失败视为成功测试。你甚至可以在修复底层代码之前编写测试。

如果 TODO 测试通过,Test::Harness 将报告“意外成功”。发生这种情况时,使用local $TODO删除 TODO 块,并将其变成真正的测试。

在污染模式下测试。

污染模式是一件有趣的事情。它是所有全局特性中最全局的。一旦你打开它,它就会影响程序中的所有代码以及所有使用的模块(以及它们使用的所有模块)。如果只有一段代码不是污染干净的,整个程序就会崩溃。考虑到这一点,确保你的模块在污染模式下工作非常重要。

让你的测试在污染模式下运行非常简单。只需在#!行中添加一个-TTest::Harness 将读取#!中的开关并使用它们来运行你的测试。

#!/usr/bin/perl -Tw

...test normally here...

当你输入make test时,它将在污染模式下运行。

脚注

  1. 第一个数字实际上没有任何意义,但它必须是 1。第二个数字才是重要的。

  2. 对于在家跟随的人来说,我使用的是 1.31 版本。它有一些错误,这很好——我们将通过我们的测试发现它们。

  3. 你实际上可以更进一步,测试手册本身。看看 Test::Inline(以前是 Pod::Tests)。

  4. 是的,测试套件中有一个错误。什么!我,编造的?

  5. 我们将在后面讨论测试列表的内容。

  6. 但是如果你的测试程序在中途崩溃了怎么办?!由于我们没有说明要运行多少个测试,我们怎么知道它失败了?不用担心,Test::More 使用了一些魔法来捕获那个死亡并将其变成失败,即使到目前为止所有测试都通过了。

  7. 我稍微整理了一下。

  8. 大多数操作系统将时间记录为自某个特定日期以来的秒数。这个日期是纪元开始的时间。Unix 的纪元从格林威治时间 1970 年 1 月 1 日午夜开始。

  9. MacOS 的纪元是 1904 年 1 月 1 日午夜。VMS 的纪元是 1858 年 11 月 17 日午夜,但 vmsperl 模拟了 Unix 纪元,所以这不是问题。

  10. 只要 SKIP 块内的代码至少可以编译。请不要问我是怎么做的。不,它不是过滤器。

  11. 不要试图使用 TODO 测试来逃避修复简单的错误!

作者

Michael G Schwern <[email protected]> 和 perl-qa 舞蹈家!

维护者

Chad Granum <[email protected]>

版权

版权所有 2001 年 Michael G Schwern <[email protected]>。

本文档是免费的;您可以在与 Perl 本身相同的条款下重新分发和/或修改它。

无论其分发方式如何,这些文件中的所有代码示例均在此置于公共领域。您被允许并鼓励在您自己的程序中使用此代码,无论是为了娱乐还是为了盈利,您都可以根据自己的意愿使用。在代码中添加一个简单的注释以表示感谢将是礼貌的,但不是必需的。