测试入门

总有一天,你会荣幸地被分配到维护编程的工作。你可能需要添加新功能或修复长期存在的错误。代码可能是你自己的,也可能是已消失很久的混乱代理人的明显混乱的咕哝。如果你还没有这样的幸运,那么下载一个大约1996年的Perl CGI脚本,尝试在use strict、警告和taint模式下运行。

维护很少是漂亮的,它混合了法医、心理学和纸牌屋的建设。复杂的问题包括向后兼容性、可移植性和互操作性。你可能不喜欢学习体验,但不要错过学习一些重要课程的机会。

  • 软件在维护上花费的时间比在开发上还要多。
  • 在开发中明显的事情很容易被掩盖。
  • 修复错误比找到错误并确保没有其他错误发生要容易。
  • 软件的真实成本是程序员的时间和精力,而不是代码长度、许可证或所需硬件。

这些规则自软件工程早期(想想Grace Hopper)以来就已经确立。好的做法仍然是:写好注释,记录你的假设和逻辑,测试你的代码到死,再测试。

每个严肃的软件工程方法论都促进了测试。它是像极限编程等新方法的关键。一个全面的测试套件有助于验证代码按预期运行,有助于最大限度地减少未来修改的范围,并让开发者能够在不改变代码行为的情况下改进代码的结构和设计。是的,这实际上可以工作。

注意事项

有经验的读者(特别是那些有强大数学背景的人)正确地指出,测试不能证明没有错误。如果理论上不可能编写一个零缺陷的非平凡程序,那么编写足够的测试来证明程序完全工作也是不可能的。程序员仍然试图编写无错误的代码,那么为什么不测试一切可能的东西呢?一个经过测试的程序可能有未知的错误,但没有经过测试的程序也会有。

当然,有些事情确实是不可测试的。这类包括“黑盒子”,如其他进程、机器和系统库。但可测试的东西比不可测试的多。Perl允许优秀的程序员执行令人恐惧的黑魔法,包括系统表和运行时操作。你可以创建一个自己的小小世界来满足可测试的接口。一点自大狂是可以的。

测试可能很困难。一块具有最小文档的代码——来自Perl 4天的设计——以及依赖于祈祷、全局变量和动物祭祀的商业逻辑可能会把你推到新的生产力高度,或者教会你如何管理程序员。如果你现在不修复它,那么你什么时候修复它?极限编程建议修改不可测试的代码,使其更容易维护和测试。有时,你必须编写简单的测试,稍作修改代码,迭代直到它可以接受。

不要让任务的庞大让你气馁。如果你能编写值得测试的Perl代码,那么你就能编写测试。

Perl模块测试如何工作

本节假设您已经熟悉perlmodinstall,并且手动安装了一个模块。在perl Makefile.PLmake阶段之后,执行make test将运行模块附带的任何测试,无论是test.pl还是以``.t”结尾的t/目录中的所有文件。将blib/目录添加到@INC中,Test::Harness将运行测试文件,捕获它们的输出,并提供一个简短的测试结果摘要,包括成功和失败。

在本质上,测试要么打印“ok”,要么打印“not ok”。就这些。任何将结果打印到标准输出的测试程序或测试框架都可以使用Test::Harness。如果您觉得有些认识论(这是编写测试时需要培养的良好品质),您可以问“什么是真理?”

        print "1..1\n";

        if (1) {
                print "ok\n";
        } else {
                print "not ok\n";
        }

基础?是的。无效?实际上并非如此。这是Perl核心测试的一个变体。如果您理解了这段代码,那么您就可以编写测试。现在忽略第一行,只需将其放入文件(truth.t)中,然后运行以下命令之一:

        perl -MTest::Harness -e "runtests 'truth.t'";

或程序

        #!/usr/bin/perl -w

        use strict;
        use Test::Harness;

        runtests 'truth.t';

这应该会显示一条消息,表明所有测试都成功了。如果没有,那么有问题,许多人会对此感兴趣并修复它。

测试的第一行对应于Test::Harness的方便测试编号功能。夹具需要知道预期的测试数量,并且组内每个单独的测试都可以有自己的编号。这是为了您的利益。如果一个测试在100个测试中神秘地失败了,那么跟踪编号93比运行调试器、注释掉大量测试套件或依赖直觉放置的打印语句要容易得多。

知道真理是好的,区分谬误也是如此。让我们稍微扩展一下truth.t。注意添加了测试编号到每一条可能的可打印行。这是一把双刃剑。

        print "1..2\n";

        if (1) {
                print "ok 1\n";
        } else {
                print "not ok 1\n";
        }

        if (0) {
                print "not ok 2\n";
        } else {
                print "ok 2\n";
        }

除了越来越重复的代码外,保持测试编号同步是痛苦的。虚假的懒惰是痛苦的——如果实际运行的测试数量与预期不符,Test::Harness将发出警告。测试编写者可能不在乎,但虚假的警告会混淆最终用户和懒惰的开发者。一般来说,输出越简单,人们越相信事情成功了。我显示器上那只毛茸茸的微笑的皮卡丘(嘿,这是来自一位迷人的女网络设计师的生日礼物)让我认为,一个巨大的黄色笑脸符号会比简单的“ok”消息更好。ASCII艺术家,打开你的编辑器!

不幸的是,真理测试是重复的且脆弱的。在第一和第二个测试之间添加第三个测试(从“真理”到“隐藏的真理”再到“谬误”的进展是有意义的)意味着复制if/else块并重新编号之前第二个测试。还存在细微的缺陷

        print "1..2\n";

        if (1) {
                print "ok 1\n";
        } else {
                print "not ok 1\n";
        }

        if ('0 but true') {
                print "ok 2\n";
        } else {
                print "not ok 2\n";
        }

        if (0) {
                print "not ok 3\n";
        } else {
                print "ok 3\n";
        }

忘记更新第一行是很常见的。预期两个测试;运行了三个测试。困惑的Test::Harness会报告奇怪的事情,如负失败百分比。小皮卡丘可能会哭。更聪明的程序员最终会应用良好的编程风格,编写他们自己的ok()函数

        print "1..3\n";

        my $testnum = 1;
        sub ok {
                my $condition = shift;
                print $condition ? "ok $testnum\n" : "not ok $testnum\n";
                $testnum++;
        }

        ok( 1 );
        ok( '0 but true' );
        ok( ! 0 );

Perl核心测试套件的最低层使用这种方法。编写起来更简单,几乎可以自动处理编号。尽管如此,它缺乏一些功能,并且更难调试。

进入Test::More

存在几个模块可以使测试更容易且几乎令人愉快。《Test》包含在现代Perl发行版中,并且与Test::Harness配合良好。《Perl-Unit》套件在Perl中重新实现了流行的JUnit框架。相对较新的Test::More模块添加了除《Test》之外的几个功能。(我承认我特别倾向于后者,尽管这些和其他模块都是不错的选择。)

Test::More有自己的ok()函数,但很少使用,而是使用更具体的函数。is()比较两个表达式。例如,测试一个加法函数就像这样

        is( add(2, 2), 4 );

这可以同样好地处理字符串和数字。自0.36版以来,它还可以区分0''(空字符串)和undef——我们希望如此。

like() 将正则表达式应用于标量。这也有助于捕获致命错误。

        $self->eat('garden salad'):

        eval { $self->write_article() };
        like( $@, qr/not enough sugar/ );

第二个参数可以是使用 qr// 操作符编译的正则表达式(自 Perl 5.005 引入),或者类似于正则表达式的字符串。允许使用修饰符。如果您绝对必须将前面的示例重写为在 Perl 5.004 上运行并使用 StudlyCaps,那么您可以

        eval { $self->write_article() };
        like( $@, '/NoT eNoUgH sUgAr/i' );

这太可爱/丑陋,不适合真正的测试,但正则表达式形式是完全有效的。

Test::More 使调试更方便

这已经很有用了,但 Test::More 还有更多功能。

Test::More 支持测试编号,就像 Test::Harness 一样,并自动提供编号。这在两种情况下是一个很大的优势:当测试套件可能意外失败(由于 die() 调用、段错误或自燃)时,或者如果测试可能意外重复(由于不正确的 chdir()、循环条件中的意外输入或时间扭曲)时。Test::Harness 很高兴警告实际运行的测试数量与其期望的不匹配。您只需要告诉它应该运行多少。这通常在使用 Test::More 时完成。

        use Test::More tests => 50;

在编写新测试时,您可能不知道会有多少。使用 no_plan 选项

        use Test::More 'no_plan';

极限编程推荐这种游戏般的做法:添加测试,运行它,编写代码通过测试,重复。完成时,更新 use 行以反映实际测试的数量。

Test::More 还能优雅地处理失败。给定以下包含最终、注定要失败的测试的文件

        use Test::More tests => 4;

        is( 1, 1 );
        is( !0, 1 );
        is( 0, 0 );
        is( undef, 1 );

单独运行 Test::More,而不是通过 Test::Harness,会产生

        1..4
        ok 1
        ok 2
        ok 3
        not ok 4
        #     Failed test (numbers.t at line 6)
        #          got: undef
        #     expected: '1'
        # Looks like you failed 1 tests of 1.

错误消息提供了包含测试的文件名、失败的测试编号、包含失败的测试的行号以及期望和接收的数据。这使得调试更容易。只需计算一次错误即可找到错误,您就会更喜欢这种方法。

Test::Harness 还支持附加到测试消息的测试注释。也就是说,它允许原始测试说

        print "ok 1 # the most basic test possible\n";

几乎所有的 Test::More 函数都支持此可选参数

        ok( 1, 'the number 1 should evaluate to true' );
        is( 2 + "2", 4, 'numeric strings should numerify in addition' );
        like( 'abc', qr/z*/, '* quantifier should match zero elements' );

这些名称仅由社会惯例所要求。把它们看作是小测试注释。如果测试是错误的,或者暴露了应该永远不会再发生的已修复的漏洞,那么描述性的名称可以使测试应该测试的内容变得明确。Test::Harness 会静默地吃掉这些名称,但它们在手动运行时会存在

        ok 1 - the number 1 should evaluate to true
        ok 2 - numeric strings should numerify in addition
        ok 3 - * quantifier should match zero elements

手动测试运行可以改善错误报告。请勿忽视这些方便的工具。

Test::More 中级功能

如果前面的功能还不够,Test::More 还支持更多功能!其中之一是可跳过的测试的概念。有时,某些标准的存在或不存在会消除测试某个特性的需要。考虑前面解释过的 qr// 操作符。需要与 Perl 5.004 兼容的模块可以通过可跳过的测试逐步降低其测试套件的复杂度

        SKIP: {
                skip( 'qr// not supported in this version', 2 ) unless $] >= 5.005;

                my $foo = qr/i have a cat/;
                ok( 'i have a caterpillar' =~ $foo,
                        'compiled regex should match similar string' );

                ok( 'i have a cold' !~ $foo,
                        'compiled regex should not match dissimilar string' );
        }

有很多要理解的东西。首先,可跳过的测试包含在一个带有标签的块中。标签必须是 SKIP。 (不用担心:您可以在文件中包含多个这些。)其次,应该有一个控制是否跳过测试的条件。此示例检查特殊变量 perlvar 以找到当前的 Perl 版本。skip() 仅在运行旧版本时调用。

skip() 函数总是以其独特的参数顺序让我困惑。第一个参数是显示每个跳过的测试的名称。第二个参数是要跳过的测试数量。这必须与块内测试的数量匹配,否则可能会引起混淆。在 Perl 5.004 上运行上述测试会产生

        ok 1 # skip qr// not supported in this version
        ok 2 # skip qr// not supported in this version

尽管消息说 ok,但 Test::Harness 会看到跳过并报告测试为跳过,而不是通过。这应该仅用于由于平台或版本差异绝对无法运行的测试。对于您目前无法解决的测试,请使用 todo()

尽管一切建立在 Test::Builder::ok() 之上,但其他函数提供了有用的快捷方式。use_ok()require_ok 会加载并可选地导入命名文件,报告成功或错误。这些函数验证模块是否存在并可编译,通常用于套件中的第一个测试。can_ok() 函数尝试解析类或对象方法。isa_ok() 检查继承。

        use_ok( 'My::Module' );
        require_ok( 'My::Module::Sequel' );

        my $foo = My::Module->new();
        can_ok( $foo->boo() );
        isa_ok( $foo, 'My::Module' );

它们会生成自己的测试名称

        ok 1 - use My::Module;
        ok 2 - require My::Module::Sequel;
        ok 3 - My::Module->can(boo)
        ok 4 - object->isa('My::Module')

其他函数和特性在 Test::More 文档中有所描述。此外,Test::Tutorial 以不同的方式解释了类似的内容。

最后,不要忘记良好的编程实践。测试函数只是标准的子例程。测试只是 Perl 代码。当它们使事情变得更简单时,使用循环、变量、辅助子例程、map() 以及其他任何东西。例如,基本的继承接口测试可以简化为

        # see if IceCreamBar inherits these methods from Popsicle
        my $icb = IceCreamBar->new();

        foreach my $method (qw( fall_off_stick freeze_tongue drip_on_carpet )) {
                can_ok( $icb, $method, "IceCreamBar should be able to $method()" );
        }

这比编写多个单独的 can_ok() 测试要好。将内容插入到测试名称中也很方便。

结论

遗憾的是,测试往往被忽视,特别是在免费软件项目中。把它看作是充足的睡眠、吃蔬菜和定期锻炼。一开始可能让你感到不舒服,但如果坚持不懈,会极大地改善事情。如果是在添加测试到没有测试的庞大系统中(比如,比如说 Perl 本身),结果可能会有所不同。

Perl 的一个目标就是让你的生活更轻松。Perl 5.8 将包括 Test::More 及其亲密的兄弟,Test::SimpleTest::Builder。它们的存在是为了使编写测试不那么麻烦,甚至更加愉快。请考虑它们。

编写和维护测试越容易,人们就越有可能这样做。更多的测试可以改善软件的可移植性、可维护性和可靠性。你现在可能将测试比作西兰花、芽甘蓝和短跑。尝试 Test::More 或其他框架,你可能逐渐将它们视为橙子、带有棉花糖的甘薯和桑拿之旅。这对你的身体真的很好。

chromatic 是 Modern Perl 的作者。在他的业余时间,他一直在帮助新手了解股票和投资

标签

反馈

这篇文章有什么问题吗?请通过在 GitHub 上打开一个问题或拉取请求来帮助我们。