Perl单元测试 - 子程序是否可测试?

时间:2017-05-26 09:26:39

标签: perl unit-testing tdd subroutine

我一直在阅读和探索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是否为空有用吗?我试图教自己如何像单位测试开发人员那样思考。

3 个答案:

答案 0 :(得分:5)

有一些事情使得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;

答案 1 :(得分:4)

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

  • 代码是否依赖于生产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话题。

答案 2 :(得分:2)

你问:

  

这个子程序是否可测试?

是的,肯定是。然而问题立即到来;你在做开发驱动测试还是测试驱动开发?让我来说明不同之处。

您目前的情况是您已经在测试之前编写了一个方法,该方法驱动开发此函数。

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

然后用最小的碎片编写方法使其编译。现在使用您正在测试的方法声明的内容完成第一个测试用例。如果你做得对,你的测试用例现在是绿色的,告诉你现在可以检查是否有重构的东西。

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

总结一下,您可以在方法中至少测试和断言两件事。

  • 断言是否返回@filelist并且不为空
  • 在您返回1
  • 时断言失败案例

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

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