首页 文章

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

提问于
浏览
7

我一直在阅读和探索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 回答

  • 4

    有几件事情使得测试 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 . 那些 Value 判断取决于你 . 但是,在分解问题之后,做出更大的改变(例如使用P4Perl)不应该像地震一样 .


    作为旁注而不是我推荐的这个问题,这是 & 和没有参数列表的用例 . 在这个"crypto context"中,子程序的参数列表是调用它的子程序的 @_ .

    这些调用继续传递链中的相同参数,这很难输入和维护:

    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;
    
  • 5

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

    • 代码是否依赖于 生产环境 Perforce安装?

    • 运行带有随机值的代码会影响 生产环境 吗?

    • 一遍又一遍地运行具有相同值的代码总会产生相同的结果吗?

    • 外部依赖关系有时可用吗?

    • 外部依赖是否超出了测试的控制范围?

    其中一些事情使得为它运行测试变得非常困难(但并非不可能) . 有些可以通过重构代码来克服 .

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

    为此构建集成测试很容易,但我上面提到的所有问题都适用 . 因为你的代码中有一个 exit ,所以你无法捕获它 . 您必须将该函数放在脚本中并运行该函数并检查退出代码,或使用类似Test::Exit的模块 .

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

    你也有输出到STDOUT,所以你需要一个工具来 grab 它 . 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
    

    正如您所看到的,它与第一个示例几乎相同 . 我也几乎不用改变测试 . 只是添加了 Mock 策略 .

    重要的是要记住,测试也是代码,同样的质量水平应该适用于它们 . 它们充当您的业务逻辑的文档,并作为您和您的开发人员(包括未来 - 您)的安全网 . 您正在测试的业务案例的清晰描述对此至关重要 .

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

  • 2

    你问:

    这个子程序是否可测试?

    是的,肯定是 . 然而问题立即到来;你在做开发驱动测试还是测试驱动开发?让我来说明一下差异 .

    您目前的情况是您已经编写了一个比测试更早的方法,这应该推动该函数的开发 .

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

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

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

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

    • 断言查看 @filelist 是否返回且不为空

    • 返回 1 时断言失败案例

    还要确保您在没有外部依赖性的情况下进行单元测试,例如文件系统等,因为这将是集成测试,其中包括测试中系统的其他移动部分 .

    最后一点,与所有事情一样,经验来自于尝试和学习 . 总是问,至少你自己,然后是你的业务伙伴,看看你是否正在测试正确的东西,以及它是否带来任何商业 Value 来测试系统的这一部分 .

相关问题