Sunday, November 4, 2012

Lerning Perl with TDD and Unit Tests

As an absolute beginner to the language I needed to write my first Perl script. As a big fan of Test-Driven Development (TDD) I thought it would be a good idea to start with a test when doing the first Perl program. And it worked really nice. This post should be a simple step-by-step tutorial for Perl beginners who want to write simple Unit Tests for Perl. I will use my first Perl script as an example.

The Example: A ClearCase trigger

To customize the behaviour of ClearCase you have to write Perl scripts which can be associated with any ClearCase command as a so called ClearCase trigger (see IBM Rational ClearCase: The ten best triggers). For my example, I needed a trigger that updates a FitNesse Wiki page (the file name is always "content.txt") when it is checked-in to ClearCase. If the file contains a string like "$Revision: \main\MAINLINE_SQE\3 $" the Perl script should update the version information. That's it. 

Step-by-Step Tutorial


  1. Install Perl.
  2. Create a folder "PerlScripts" for the new Perl scripts. We will have two files in this folder: "CiVersionFitnesseTrigger.pl" is the Perl script for the trigger. "CiVersionFitnesseTriggerTests.pl" is the Perl script for the corresponding Unit Tests.  
  3. Download the Test::Simple Perl module. Unpack the gz archive. We will only need the file "Simple.pm" from the folder "lib/Test". Create a folder "Test" as sub folder of our "PerlScripts" folder. Copy the file "Simple.pm" to this "Test" folder.
We start writing our first test in "CiVersionFitnesseTriggerTests.pl":
use Test::Simple tests => 1;

# System under test
require 'CiVersionFitnesseTrigger.pl'; 

# Testing is_fitnesse_wiki_page() method
ok(FitTrigger::is_fitnesse_wiki_page('content.txt'), 'content.txt is page');

We start defining an empty sub routine and an empty main routine in "CiVersionFitnesseTrigger.pl":
package FitTrigger;

sub is_fitnesse_wiki_page {
 return  0;
}

#
# Main method
#
1;
We can now run the first unit test and see it failing:
Now we have the infrastructure to start implementation. We fix the first failing test:
package FitTrigger;

sub is_fitnesse_wiki_page {
 my ($file_name) = @_;
 return  $file_name =~ m/^(.*\\)?content\.txt$/
}

#
# Main method
#
1;
Now run the unit test again and it succeeds:
We continue the cycle of writing new unit tests and implementing the script step by step. In the end we have 12 unit tests and 1 integration test:
use Test::Simple tests => 13;

# System under test
require 'CiVersionFitnesseTrigger.pl'; 

# Testing is_fitnesse_wiki_page() method
ok(FitTrigger::is_fitnesse_wiki_page('content.txt'), 'content.txt is page');
ok(FitTrigger::is_fitnesse_wiki_page('c:\content.txt'), 'c:\content.txt is page');
ok(FitTrigger::is_fitnesse_wiki_page('..\content.txt') , '..\content.txt is page');
ok(FitTrigger::is_fitnesse_wiki_page('c:\temp\content.txt'), 'c:\temp\content.txt is page');
ok(!FitTrigger::is_fitnesse_wiki_page('content.txt.old') , 'content.txt.old is not a page');
ok(!FitTrigger::is_fitnesse_wiki_page('somecontent.txt') , 'somecontent.txt is not a page');
ok(!FitTrigger::is_fitnesse_wiki_page('content.txt\something.txt') , 'content.txt\something.txt is not a page');

# Testing getTempFolder() method
my $tmpFolder = FitTrigger::get_temp_folder();
ok(defined($tmpFolder) && $tmpFolder ne '' && length($tmpFolder) > 1 , 'temporary folder not empty');

# Testing getTempFile() method
my $tmpFile = FitTrigger::get_temp_file();
ok(defined($tmpFile) && $tmpFile ne '' && length($tmpFile) > 1 , 'temporary file not empty');

# Testing update_revision_in_target() method
my $testFile = "$tmpFolder\\test.txt";
my $targetFile = "$tmpFolder\\target.txt";
open("TESTFILE", ">$testFile") ||
    &error("Could not open test File $testFile for writing");
print TESTFILE "hallo1\nhallo2\n\$Revision: VERSION_ZZZ \$\n";
close TESTFILE;
my $newVersion = 'VERSION_111';
FitTrigger::update_revision_in_target($testFile,$targetFile,$newVersion);
open(F,"$targetFile");
my @list = ;
my $content=join('',@list);
close F;
my $expectedContent = "hallo1\nhallo2\n\$Revision: VERSION_111 \$\n";
ok($content eq $expectedContent, 'version was updated in target file');


# Testing overwrite_file() method
FitTrigger::overwrite_file($targetFile,$testFile);
open(F2,"$testFile");
@list=;
my $newContent =join('',@list);
close F2;
ok($newContent eq $expectedContent, 'file was overwritten with a modified file');
ok(! -e $targetFile, 'modified file is deleted');

# Testing main() method
$testFile = "$tmpFolder\\content.txt";
open("TESTFILE", ">$testFile") ||
    &error("Could not open test File $testFile for writing");
print TESTFILE "hallo1\nhallo2\n\$Revision: VERSION_ZZZ \$\n";
close TESTFILE;
$ENV{CLEARCASE_PN}=$testFile;
$ENV{CLEARCASE_ID_STR}='VERSION_888';
system ("perl CiVersionFitnesseTrigger.pl");
my $expectedContentMain = "hallo1\nhallo2\n\$Revision: VERSION_888 \$\n";
open(F3,"$testFile");
@list=;
my $newContentMain =join('',@list);
close F3;
ok($newContentMain eq $expectedContentMain, 'perl script has updated content.txt');

The complete implementation in "CiVersionFitnesseTrigger.pl" looks like:
package FitTrigger;

sub is_fitnesse_wiki_page {
 my ($file_name) = @_;
 return  $file_name =~ m/^(.*\\)?content\.txt$/
}

sub get_temp_folder {
 my $tmp_folder = $ENV{TMP};
 $tmp_folder = $ENV{TEMP} unless ($tmp_folder);
 $tmp_folder = "/tmp" unless ($tmp_folder);
 return $tmp_folder;
}

sub get_temp_file {
 my $tmp_folder = get_temp_folder();
 return "$tmpFolder\\ccTriggerTmp.$$";
}

sub update_revision_in_target {
 my $source = @_[0];
 my $target = @_[1];
 my $revision = @_[2];
 open("SOURCE", "$source") ||
  &error("Could not open source file $source for reading");
 open("TARGET", ">$target") ||
  &error("Could not open target file $target for reading");
 while ()
 {
  if (/\$Revision:?.*\$/) {
                    s/\$Revision:?.*\$/\$Revision: $revision \$/;
         }
  print TARGET;
 }
 close SOURCE;
 close TARGET;
}

sub overwrite_file {
 my $source = @_[0];
 my $target = @_[1];
 open (SOURCE, "$source") ||
  &error ("Could not open source file $source for reading");
 open (TARGET, ">$target") ||
  &error ("Could not open target file $target for writing");
 while() {
  print TARGET;
 }
 close(SOURCE);
 close(TARGET);
 unlink($source);
}

sub error {
    my ($message) = @_;
    die ($message."\nUnable to continue checkin ...\n");
}


#
# Main method
#
# Summary: 
# If the name of the checkin file is ‘content.txt’ then search in the content of the file for a string like
# „$Revision: \main\MAINLINE_22_WIPID\4 $“. This string will then be replaced 
# with e.g. „$Revision: \main\MAINLINE_22_WIPID\5 $“.  

my $check_in_file = $ENV{'CLEARCASE_PN'};
my $revision = $ENV{'CLEARCASE_ID_STR'};

if(is_fitnesse_wiki_page($check_in_file)) {
 my $targetFile = get_temp_file();
 update_revision_in_target($check_in_file,$targetFile,$revision);
 overwrite_file($targetFile,$check_in_file);
}
1;
Running the tests: