我一直在阅读和探索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是否为空有用吗?我试着教自己如何像一个单元测试开发人员一样思考。
发布于 2017-05-26 14:35:33
有几件事情使得perforce_filelist子程序的测试比需要的要困难得多:
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" }但是,你也必须判断这种分解水平对你是否值得。对于你很少使用的个人工具来说,这可能是太多的工作了。对于一些你必须支持和很多人使用的东西来说,这可能是值得的。在这种情况下,您可能需要官方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 );对于&和not参数列表(甚至()),它将@_传递到下一个级别:
my @new_files = perforce_filelist( $path, $date );
my @filelist = &get_p4_files;
my $command = &make_p4_files_command;发布于 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,所以您也需要一个工具来抓取。测试::输出可以做到这一点。
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{}。如果没有该模块,这可以手动完成。如果您想知道如何实现,请查看模块的代码。
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进行测试的策略,以及不应该做什么,我建议您看https://www.youtube.com/watch?v=4kMySZv65gc by 柯蒂斯·坡的talk。
发布于 2017-05-26 09:36:56
你问:
这个子例程可测试吗?
是的,绝对是。然而,一个问题马上就会出现:您是在进行开发驱动的测试还是测试驱动的开发?让我来说明不同之处。
当前的情况是,您已经编写了一个比测试更早的方法,它应该会驱动这个函数的开发。
如果您试图遵循TDD的基本指导,您应该首先编写您的测试用例。在这个阶段,单元测试的结果将是红色的,因为有缺失的部分需要测试。
然后用最少的零碎来编写方法,使其编译。现在,用您正在测试的方法中断言的内容来完成第一个测试用例。如果您做对了,您的测试用例现在是绿色的,这表明您现在可以检查是否存在要重构的东西。
这将给你的TDD的基本原则,即:红色,绿色和重构。
总之,您可以在方法中测试和断言至少两件事情。
@filelist是否返回且不为空。1时断言失败案例还要确保您是没有外部依赖项的单元测试,如文件系统等,因为这将是集成测试,这包括测试中系统的其他移动部分。
最后一点,就像每件事一样,经验来自尝试和学习。一定要问,至少是你自己,然后是你的业务同行,看看你是否在测试正确的东西,以及它是否为测试系统的这一部分带来了任何业务价值。
https://stackoverflow.com/questions/44198069
复制相似问题