10

这是我对 Stack Overflow 的第一个问题。如果我违反了一些规则,请提前道歉。

我一直在阅读 Intermediate Perl,第 2 版的第 14 章,其中讨论了测试 Perl 模块和使用来自 Test::More 的特性。我指的是直接在本书中标题为“添加我们的第一个测试”的部分中发布的代码。

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

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

sound方法是为特定动物返回的简单字符串,例如,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"
    );
}

测试失败。我运行了所有的构建命令,但是在运行“构建测试”时,动物测试失败了:

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

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

#   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。

4

3 回答 3

5

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

看,这个子程序:

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

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

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

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

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

于 2012-10-19T00:20:07.327 回答
4

您已经发现您拨打电话的问题在于您拨打电话时is使用了错误的包裹。完全指定函数名称,就像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动脉瘤时乱扔垃圾,print被重定向以修改标量而不是实际打印到屏幕上,现在测试由于正确的原因而失败,即:字符串的大小写不匹配。

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

[在动物中:]

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直接调用 built-in ,而是Animal调用$class->print,而后者又调用 built-in print。然后子类Foofle覆盖该print方法以返回其参数而不是打印它们,这使测试代码可以访问要打印的内容。

这种方法比必须修改全局变量来确定要打印的内容要干净得多,但它确实有两个缺点:它需要修改被测代码以使其更具可测试性,并且它从不实际测试打印是否发生。它只是测试print用正确的参数调用的。因此,Animal::print 必须如此微不足道,以至于通过检查显然是正确的。

于 2012-10-19T04:13:22.017 回答
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::is, main::ok,main::done_testing等定义函数。

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

一种解决方法是也导入Test::MoreFoofle's 命名空间。

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

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

{
    package Foofle;
    Test::More::is( something, something_else );
}
于 2012-10-19T00:21:25.137 回答