首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >Perl单元测试--子例程可测试性吗?

Perl单元测试--子例程可测试性吗?
EN

Stack Overflow用户
提问于 2017-05-26 09:26:38
回答 3查看 1.1K关注 0票数 7

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

代码语言:javascript
复制
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是否为空有用吗?我试着教自己如何像一个单元测试开发人员一样思考。

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2017-05-26 14:35:33

有几件事情使得perforce_filelist子程序的测试比需要的要困难得多:

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

但是,您的子例程的责任是获取一个文件列表并返回它。你做的任何事情都会增加测试的难度。如果您无法改变这种情况,因为您无法控制这种情况,那么您可以在将来编写这样的东西:

代码语言:javascript
复制
#!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" }

但是,你也必须判断这种分解水平对你是否值得。对于你很少使用的个人工具来说,这可能是太多的工作了。对于一些你必须支持和很多人使用的东西来说,这可能是值得的。在这种情况下,您可能需要官方P4Perl API。这些价值判断由你来决定。但是,在分解了问题后,做出更大的更改(例如使用P4Perl)不应该像地震一样。

作为附带说明,而不是我为这个问题推荐的东西,这是&的用例,没有参数列表。在这个“密码上下文”中,子例程的参数列表是调用它的子例程的@_

这些调用继续在链中传递相同的参数,这对于输入和维护是很烦人的:

代码语言:javascript
复制
    my @new_files = perforce_filelist( $path, $date );
    my @filelist = get_p4_files( $path, $date ); 
    my $command = make_p4_files_command( $path, $date );

对于&和not参数列表(甚至()),它将@_传递到下一个级别:

代码语言:javascript
复制
    my @new_files = perforce_filelist( $path, $date );

    my @filelist = &get_p4_files; 
    my $command = &make_p4_files_command;
票数 5
EN

Stack Overflow用户

发布于 2017-05-26 10:52:40

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

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

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

定义您想要测试的内容也很重要。函数的https://en.wikipedia.org/wiki/Unit_testing将确保它根据放入的内容返回正确的内容,但控制外部依赖项。另一方面,https://en.wikipedia.org/wiki/Integration_testing将运行外部依赖项。

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

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

您也有输出到STDOUT,所以您也需要一个工具来抓取。测试::输出可以做到这一点。

代码语言:javascript
复制
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;

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

编写单元测试也可以类似地完成。有测试::Mock::Cmd来用另一个函数替换backticks或qx{}。如果没有该模块,这可以手动完成。如果您想知道如何实现,请查看模块的代码。

代码语言:javascript
复制
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的机器上运行此测试,如果该功能只占整个应用程序的一小部分,并且在处理应用程序的不同部分时仍然希望运行完整的测试套件,则该测试非常有用。

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

代码语言:javascript
复制
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进行测试的策略,以及不应该做什么,我建议您看https://www.youtube.com/watch?v=4kMySZv65gc by 柯蒂斯·坡的talk。

票数 4
EN

Stack Overflow用户

发布于 2017-05-26 09:36:56

你问:

这个子例程可测试吗?

是的,绝对是。然而,一个问题马上就会出现:您是在进行开发驱动的测试还是测试驱动的开发?让我来说明不同之处。

当前的情况是,您已经编写了一个比测试更早的方法,它应该会驱动这个函数的开发。

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

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

这将给你的TDD的基本原则,即:红色,绿色和重构。

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

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

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

最后一点,就像每件事一样,经验来自尝试和学习。一定要问,至少是你自己,然后是你的业务同行,看看你是否在测试正确的东西,以及它是否为测试系统的这一部分带来了任何业务价值。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/44198069

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档