【问题标题】:Perl Unit Testing -- is the subroutine testable?Perl 单元测试——子程序是否可测试?
【发布时间】:2017-05-26 09:26:38
【问题描述】:

我一直在阅读和探索 Perl 中单元测试和测试驱动开发的概念。我正在研究如何将测试概念整合到我的开发中。假设我在这里有一个 Perl 子例程:

sub perforce_filelist {

    my ($date) = @_;

    my $path = "//depot/project/design/...module.sv";
    my $p4cmd = "p4 files -e $path\@$date,\@now";

    my @filelist = `$p4cmd`; 

    if (@filelist) {
        chomp @filelist;
        return @filelist;
    }
    else {
        print "No new files!"
        exit 1;
    }
}

子例程执行 Perforce 命令并将该命令的输出(文件列表)存储到 @filelist 数组中。这个子程序可以测试吗?测试返回的@filelist 是否为空有用吗?我正在尝试自学如何像单元测试开发人员一样思考。

【问题讨论】:

  • 你想知道如何为这个特定的东西编写测试吗?您可以在测试环境中运行 Perforce 命令,还是在生产环境中运行?您可以根据需要安全地重新运行它吗?结果可重现吗?

标签: perl unit-testing tdd subroutine


【解决方案1】:

有几件事使测试perforce_filelist 子例程比它需要的更困难:

  • p4 路径是硬编码的
  • p4 命令在子程序内部构造
  • p4 命令是固定的(因此,它始终是路径中的第一个 p4
  • 你直接从子程序输出
  • 您从子程序内部退出

但是,您的子程序的职责是获取一个文件列表并返回它。您在此之外所做的任何事情都会使测试变得更加困难。如果由于无法控制而无法更改此内容,则将来可以编写类似的内容:

#!perl -T

# Now perforce_filelist doesn't have responsibility for
# application logic unrelated to the file list 
my @new_files = perforce_filelist( $path, $date );
unless( @new_files ) {
    print "No new files!"; # but also maybe "Illegal command", etc
    exit 1;
    }

# Now it's much simpler to see if it's doing it's job, and
# people can make their own decisions about what to do with
# no new files.
sub perforce_filelist {
    my ($path, $date) = @_;
    my @filelist = get_p4_files( $path, $date ); 
    }

# Inside testing, you can mock this part to simulate
# both returning a list and returning nothing. You 
# get to do this without actually running perforce.
#
# You can also test this part separately from everything
# else (so, not printing or exiting)
sub get_p4_files {
    my ($path, $date) = @_;
    my $command = make_p4_files_command( $path, $date );
    return unless defined $command; # perhaps with some logging
    my @files = `$command`;
    chomp @files;
    return @files;
    }   

# This is where you can scrub input data to untaint values that might
# not be right. You don't want to pass just anything to the shell.
sub make_p4_files_command {
    my ($path, $date) = @_;
    return unless ...; # validate $path and $date, perhaps with logging
    p4() . " files -e $path\@$date,\@now";
    }

# Inside testing, you can set a different command to fake
# output. If you are confident the p4 is working correctly,
# you can assume it is and simulate output with your own
# command. That way you don't hit a production resource.        
sub p4 { $ENV{"PERFORCE_COMMAND"} // "p4" }

但是,您还必须判断这种级别的分解对您来说是否值得。对于您不经常使用的个人工具,它可能工作量太大。对于您必须支持并且很多人使用的东西,这可能是值得的。在这种情况下,您可能需要official P4Perl API。这些价值判断取决于你。但是,在分解了问题之后,进行更大的更改(例如使用 P4Perl)不应该是地震。


作为旁注而不是我针对此问题推荐的内容,这是& 的用例,并且没有参数列表。在这个“加密上下文”中,子程序的参数列表是调用它的子程序的@_

这些调用不断在链中传递相同的参数,这很烦人输入和维护:

    my @new_files = perforce_filelist( $path, $date );
    my @filelist = get_p4_files( $path, $date ); 
    my $command = make_p4_files_command( $path, $date );

使用& 并且没有参数列表(甚至没有()),它将@_ 传递到下一个级别:

    my @new_files = perforce_filelist( $path, $date );

    my @filelist = &get_p4_files; 
    my $command = &make_p4_files_command;

【讨论】:

  • 我考虑在我的答案中将问题分解为多个子例程,但我认为这太长了。这个答案做得很好,解释得很好。
  • 哇,感谢您的清晰解释。如果我自己阅读这些东西,我正在学习一些需要几周时间的东西
【解决方案2】:

它是否可测试很大程度上取决于您的环境。您需要问自己以下问题:

  • 代码是否依赖生产 Perforce 安装?
  • 使用随机值运行代码会干扰生产吗?
  • 一遍又一遍地运行具有相同值的代码是否总是产生相同的结果?
  • 外部依赖有时会不可用吗?
  • 外部依赖是否超出了测试的控制范围?

其中一些因素使得运行测试变得非常困难(但并非不可能)。有些可以通过稍微重构代码来克服。

定义您想要测试的确切内容也很重要。该函数的unit test 将确保它根据您输入的内容返回正确的内容,但您可以控制外部依赖项。另一方面,integration test 将运行外部依赖项。

为此构建集成测试很容易,但我上面提到的所有问题都适用。而且由于您的代码中有exit,因此您无法真正捕获它。您必须将该函数放在脚本中并运行它并检查退出代码,或者使用像 Test::Exit 这样的模块。

您还需要以始终获得相同结果的方式设置 Perforce。这可能意味着那里有您控制的日期和文件。我不知道 Perforce 是如何工作的,所以我不能告诉你如何做到这一点,但总的来说这些东西被称为fixtures。这是您控制的数据。对于数据库,您的测试程序会在运行测试之前安装它们,因此您可以获得可重现的结果。

你也有输出到 STDOUT,所以你也需要一个工具来获取它。 Test::Output 可以做到。

use Test::More;
use Test::Output;
use Test::Exit;

# do something to get your function into the test file...

# possibly install fixtures...
# we will fake the whole function for this demonstration

sub perforce_filelist {
    my ($date) = @_;

    if ( $date eq 'today' ) {
        return qw/foo bar baz/;
    }
    else {
        print "No new files!";
        exit 1;
    }
}

stdout_is(
    sub {
        is exit_code( sub { perforce_filelist('yesterday') } ),
            1, "exits with 1 when there are no files";
    },
    "No new files!",
    "... and it prints a message to the screen"
);

my @return_values;
stdout_is(
    sub {
        never_exits_ok(
            sub {
                @return_values = perforce_filelist('today');
            },
            "does not exit when there are files"
        );
    },
    q{},
    "... and there is no output to the screen"
);
is_deeply( \@return_values, [qw/foo bar baz/],
    "... and returns a list of filenames without newlines" );

done_testing;

如您所见,这可以相对轻松地处理该函数所做的所有事情。我们涵盖了所有代码,但我们依赖于外部的东西。所以这不是真正的单元测试。

编写单元测试可以类似地完成。有Test::Mock::Cmd 替换反引号或qx{} 用另一个函数。这也可以在没有该模块的情况下手动完成。如果你想知道如何查看模块的代码。

use Test::More;
use Test::Output;
use Test::Exit;

# from doc, could be just 'return';
our $current_qx = sub { diag( explain( \@_ ) ); return; };
use Test::Mock::Cmd 'qx' => sub { $current_qx->(@_) };

# get the function in, I used yours verbatim ...

my $qx; # this will store the arguments and fake an empty result
stdout_is(
    sub {
        is(
            exit_code(
                sub {
                    local $current_qx = sub { $qx = \@_; return; };
                    perforce_filelist('yesterday');
                }
            ),
            1,
            "exits with 1 when there are no files"
        );
    },
    "No new files!",
    "... and it prints a message to the screen"
);
is $qx->[0], 'p4 files -e //depot/project/design/...module.sv@yesterday,@now',
    "... and calls p4 with the correct arguments";

my @return_values;
stdout_is(
    sub {
        never_exits_ok(
            sub {
                # we already tested the args to `` above, 
                # so no need to capture them now
                local $current_qx = sub { return "foo\n", "bar\n", "baz\n"; };
                @return_values = perforce_filelist('today');
            },
            "does not exit when there are files"
        );
    },
    q{},
    "... and there is no output to the screen"
);
is_deeply( \@return_values, [qw/foo bar baz/],
    "... and returns a list of filenames without newlines" );

done_testing;

我们现在可以直接验证是否调用了正确的命令行,但我们不必费心将 Perforce 设置为实际拥有任何文件,这使测试运行更快并让您独立。您可以在未安装 Perforce 的机器上运行此测试,如果该功能只是整个应用程序的一小部分,并且您在处理不同部分时仍希望运行完整的测试套件,这将非常有用应用程序。


让我们快速看一下第二个示例的输出。

ok 1 - exits with 1 when there are no files
ok 2 - ... and it prints a message to the screen
ok 3 - ... and calls p4 with the correct arguments
ok 4 - does not exit when there are files
ok 5 - ... and there is no output to the screen
ok 6 - ... and returns a list of filenames without newlines
1..6

如您所见,它与第一个示例几乎相同。我也几乎不必更改测试。只是添加了模拟策略。

请务必记住,测试也是代码,同样的质量水平也应该适用于它们。它们充当您的业务逻辑的文档,并为您和您的开发人员(包括未来的您)提供安全网。为此,必须清楚地描述您正在测试的业务案例。

如果您想了解更多关于使用 Perl 进行测试的策略,以及不该做什么,我建议您观看 Testing Lies 的讲座 Curtis Poe

【讨论】:

  • 附带说明,我从未使用过我之前提到的三个特殊的Test:: 模块中的任何一个。事实上,我只知道其中一个。检查 CPAN 上有哪些有用的东西总是值得的。 :)
【解决方案3】:

你问:

这个子程序可以测试吗?

是的,确实如此。然而一个问题马上就来了;你在做开发驱动测试还是测试驱动开发?让我来说明一下区别。

你现在的情况是你在测试之前写了一个方法,应该驱动这个函数的开发。

如果您尝试遵循 TDD 的基本指导,则应先编写测试用例。在这个阶段,你的单元测试的结果将是红色的,因为有缺失的部分需要测试。

然后你用最少的点点滴滴编写方法以使其编译。现在用你从你正在测试的方法中断言的东西来完成第一个测试用例。如果你做对了,你的测试用例现在是绿色的,这表明你现在可以检查是否有需要重构的东西。

这将为您提供 TDD 的基本原理,即;红色、绿色和重构。

总之,你可以在你的方法中测试和断言至少两件事。

  • 断言查看@filelist是否返回且不为空
  • 返回1 时断言失败案例

还要确保您是单元测试,没有外部依赖项,如文件系统等,因为那将是集成测试,其中包括其他移动部分系统在您的测试中。

最后一点,就像所有事情一样,经验来自于尝试和学习。始终询问,至少是您自己,然后是您的业务同行,看看您是否在测试正确的东西,以及它是否为测试系统的该部分带来任何商业价值。

【讨论】:

  • 他们没有返回一个,他们退出程序。这很棘手。
  • 如何使用 return 1 退出子程序?
猜你喜欢
  • 2011-06-12
  • 2011-05-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多