我一直在阅读和探索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
是否为空有用吗?我试图教自己如何像单位测试开发人员那样思考。
答案 0 :(得分:5)
有一些事情使得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" }
但是,您还必须判断这种分解程度是否值得。对于不经常使用的个人工具,可能工作量太大。对于你必须支持并且很多人使用的东西,它可能是值得的。在这种情况下,您可能需要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)
它是否可测试取决于您的环境。您需要问自己以下问题:
其中一些事情使得为它运行测试变得非常困难(但并非不可能)。有些可以通过重构代码来克服。
定义您想要测试的内容也很重要。该函数的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
还要确保您没有外部依赖项单元测试,例如文件系统等,因为这将是集成测试,其中包括其他移动部分你的测试中的系统。
作为最后一点,与所有事情一样,经验来自于尝试和学习。始终要问,至少是你自己,然后是你的同事,看看你是否正在测试正确的东西,以及它是否带来任何商业价值来测试系统的这一部分。