使用Test :: More测试Perl模块(中级Perl,第14章)

时间:2012-10-19 00:07:06

标签: perl testing module

这是Stack Overflow的第一个问题。如果我打破一些规则,请提前道歉。

我一直在阅读Intermediate Perl的第14章,第2版,其中讨论了测试Perl模块和使用Test :: More的功能。我指的是在本书的“添加我们的第一次测试”一节中直接发布的代码。

对于某些背景知识,在本章中,将在具有相同名称的模块中创建示例Animal类。这个类有一个简单的speak方法,如下所示:

sub speak {
    my $class = shift;
    print "a $class goes ", $class->sound, "!\n";
}

sound方法是为特定Animal返回的简单字符串,因此,例如,Horse的sound方法将只是sub sound { "neigh" }并且它的speak方法应该输出以下内容:

A Horse goes neigh!

我遇到的问题如下:在我在./Animal/t/Animal.t创建的测试代码中,我被指示使用裸块和Test::More::is来测试speak方法正在运行。代码在测试文件中如下所示:

[test code snip]
{
    package Foofle;
    use parent qw(Animal);

    sub sound { 'foof' }
    is( Foofle->speak, 
        "A Foofle goes foof!\n", 
        "An Animal subclass does the right thing"
    );
}

测试失败。我运行了所有Build命令,但是当运行“Build test”时,我对Animal测试失败了:

Undefined subroutine &Foofle::is called at t/Animal.t line 28.

当我尝试明确使用Test::More::is而不仅仅是普通is时,测试仍然失败并显示以下消息:

#   Failed test 'An Animal subclass does the right thing'
#   at t/Animal.t line 28.
#          got: '1'
#     expected: 'A Foofle goes foof!
# '

我的方法似乎与我解释的完全一致。我认为第一个错误是范围问题,因为裸块,但不是100%肯定。第二个错误我不确定,因为如果我要创建一个Foofle类作为Animal的孩子并在其上调用speak,我就不会得到1回复,而是预期的产出。

有人能帮助解决我可能做错的事吗?对于可能相关的软件版本,我使用的是perl v5.16,Test :: More v0.98和Module :: Starter v1.58。

3 个答案:

答案 0 :(得分:5)

您已正确解释了第一个错误的原因,并将其修正了(指定了正确的包名称)。但是你似乎错过了一个简单的事实:Animal类的speak方法没有return这个a $class goes...字符串 - 它返回打印它的结果(1)而不是!

请参阅此子例程:

sub speak {
    my $class = shift;
    print "a $class goes ", $class->sound, "!\n";
}

...没有明确的return语句。在这种情况下,返回的是评估子例程的最新调用语句的结果 - 即,评估print something的结果,is 1true,实际上)。< / p>

这就是测试失败的原因。您可以通过测试1来修复它(但我觉得这太微不足道了)或更改方法本身,以便返回打印的字符串。例如:

sub speak {
    my $class = shift;
    my $statement = "a $class goes " . $class->sound . "!\n";
    print $statement;
    return $statement;
}

......而且,坦率地说,这两种方法看起来有点......可疑。后者虽然显然更完整,但实际上并不涵盖这种speak方法的所有功能:它只测试语句是否正确,而不是是否打印。 )

答案 1 :(得分:4)

您已经知道拨打is的问题是您拨打电话时出错了。完成工作时完全指定函数名称,将<{1}}导入到命名空间中,方法是

is

测试包中的某处。

你问题的其余部分的答案在于你所测试的内容与你正在做的事情之间的区别。打印了use Test::More; 的内容,但当您询问speak时,您询问的是is(speak, ...)返回的内容,这与其打印的内容无关。它实际上是speak的非常有用的返回值。

由于print的目的是打印特定的字符串,因此speak的测试应该测试它确实打印了一个字符串并且它是正确的字符串。但是,为了进行测试,您需要一些方法来捕获打印的内容。

实际上有几种方法可以做到这一点,从使用speak强制您指定要打印到的文件句柄到将IO::File的替代品修补到您的类中,但是以下技术不需要对被测系统进行任何修改以提高其可测试性。

print内置版允许您更改select打印的位置。默认输出通道是print,但您通常会假装您不知道那种事情。幸运的是,您也可以使用STDOUT来发现原始文件句柄,尽管您应该确保恢复默认文件句柄(毕竟是全局变量),即使您的测试由于某种原因而死亡。所以你需要管理异常。你需要一个文件句柄,你可以检查内容,而不一定实际打印任何东西; select可以帮助那里。

通过这种方法,您最终可以使用

测试原始代码
IO::Scalar

package AnimalTest; use IO::Scalar; use Test::More tests => 1; use Try::Tiny; { package Foofle; use base qw(Animal); sub sound { 'foof' } } { my $original_FH = select; try { my $result; select IO::Scalar->new(\$result); Foofle->speak(); is( $result, "A Foofle goes foof!\n", "An Animal subclass does the right thing" ); } catch { die $_; } finally { select $original_FH; }; } 确保您在Try::Tiny碰巧给speak动脉瘤时不会乱丢,Animal被重定向以修改标量而非实际打印到屏幕,现在测试失败的原因是正确的:字符串的大小写不匹配。

你会注意到有很多设置涉及;这是因为被测系统的可测性并不是特别好,因此我们必须进行补偿。在我自己的代码中,这不是我选择的方法,而是选择使原始代码更易于测试。然后进行测试,我通常使用TMOE进行猴子补丁(即覆盖其中一个被测试的方法)。这种方法看起来更像是这样:

[动物:]

print

[后:

sub speak {
    my $class = shift;
    $class->print("a $class goes ", $class->sound, "!\n");
}

sub print {
    my $class = shift;
    print @_;
}

您会注意到这看起来更像您的原始代码。主要区别在于,{ package Foofle; use base qw(Animal); sub sound { 'foof' } sub print { my ($self, @text) = @_; return join '', @text; } } is( Foofle->speak(), "A Foofle goes foof!\n", "An Animal subclass does the right thing" ); 不是直接调用内置print,而是调用Animal,而$class->print会调用内置print。子类Foofle然后覆盖print方法以返回其参数而不是打印它们,这使测试代码可以访问打印的内容。

这种方法要比修改全局变量更清晰,以便弄清楚要打印的内容,但它确实有两个缺点:它需要修改测试中的代码以使其更易于测试,并且它实际上从未测试是否打印发生。它只测试用正确的参数调用print。因此,Animal :: print必须是如此微不足道,以至于通过检查显然是正确的。

答案 2 :(得分:2)

我想你的代码看起来像:

package SomeTest;   # if omitted, it's like saying "package main"
use Test::More;
...
{
    package Foofle;
    is( something, something_else );
}

use Test::More语句会将一些Test::More函数导出到调用命名空间,在本例中为SomeTest(或main)。这意味着将为符号main::ismain::okmain::done_testing等定义函数。

在以package Foofle开头的块中,您现在位于Foofle命名空间中,因此现在Perl将查找与符号Foofle::is对应的函数。它不会找到一个,所以它会抱怨并退出。

一种解决方法是将Test::More导入Foofle的命名空间。

{
    package Foofle;
    use Test::More;
    is( something, something_else );
}

另一个是使用完全限定的方法名称来调用is

{
    package Foofle;
    Test::More::is( something, something_else );
}