I have been reading up and exploring on the concept of unit testing and test driven development in Perl. I'm looking into how I can incorporate the testing concepts into my development. Say I have a Perl subroutine here:
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;
}
}
The subroutine executes a Perforce command and stores the output of that command (which is a list of files) in to the @filelist
array. Is this subroutine testable? Would testing if the returned @filelist
is empty useful? I am trying to teach myself how to think like a unit test developer.
There are a couple of things that make testing that perforce_filelist
subroutine more difficult than it needs to be:
p4
in the path)But, your subroutine's responsibility is to get a filelist and return it. Anything you do outside of that makes it harder to test. If you can't change this because you don't have control of that, you can write stuff like this in the future:
#!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" }
But, you also have to judge if this level of decomposition is worth it to you. For a personal tool that you use infrequently, it's probably too much work. For something you have to support and lots of people use, it might be worth it. In that case, you might want the official P4Perl API. Those value judgements are up to you. But, having decomposed the problem, making bigger changes (such as using P4Perl) shouldn't be as seismic.
As a side note and not something I'm recommending for this problem, this is the use case for the &
and no argument list. In this "crypto context", the argument list to the subroutine is the @_
of the subroutine calling it.
These calls keep passing on the same arguments down the chain, which is annoying to type out and maintain:
my @new_files = perforce_filelist( $path, $date );
my @filelist = get_p4_files( $path, $date );
my $command = make_p4_files_command( $path, $date );
With the &
and no argument list (not even ()
), it passes on the @_
to the next level:
my @new_files = perforce_filelist( $path, $date );
my @filelist = &get_p4_files;
my $command = &make_p4_files_command;