TDD a perlscript with modulinos (PWC-118)
Perl is a language that has a strong testing culture, the CPAN Testing Service (CPANTS) for example is amazing. When you release any module it gets tested on a huge variety of systems and operating systems and Perl versions automatically and for free.
Perl also has great testing tools, Test:: family of modules are hard to beat.
Writing a new module in a Test Driven Development (TDD) style is very much possible and is my preferred way of tackling the Perl Weekly Challenge when I put time aside to take them on. What you can also do is TDD the script itself via a "modulino".
This week's challenge is a script to tell you if the number you provide is a binary palindrome, you'll probably want that context for the code examples that follow. :-)
To make this possible you can wrap all the code in your script in a sub (for example run()). then call that sub only when the code is being called from Perl, rather than from the test suite. But first we want to start with a test.
use Test::More;
require_ok('./ch-1.pl');
done_testing;
This uses the standard Test::More module for "require_ok" so we can load the script itself. This test will fail till you create the script itself.
As I am creating a script that outputs to the screen (stdout) I want to test the output of the command so I use the Test::Output module.
use Test::More;
use Test::Output;
require_ok('./ch-1.pl');
stdout_is { &run() }
'foo',
'Dumb test to see if the test works';
done_testing;
So then we can expand the script to look like this:
__PACKAGE__->run() unless caller;
sub run {
print "foo";
}
1;
From here we can take small steps towards out goal whilst having the safety of tests. So the test might become more sensible like this:
use Test::More;
use Test::Output;
require_ok('./ch-1.pl');
stdout_is { &run(5) }
'xxxxx',
'5 is a palindrome';
done_testing;
This is a small test that confirms that the script gives the desired answer in the format we want. In this case I have already coded up the module in a TDD style; so this is really about testing the wiring up of the code. Version one might look like this:
__PACKAGE__->run() unless caller;
use lib './lib';
use Binary::Palindrome;
sub run {
my $n = $ARGV[0] || shift;
my $bp = Binary::Palindrome->new;
print $bp->is_palindrome($n);
}
1;
So in this still more than I actually did in step one. I actually just added the use lines and ran the tests to see if everything stayed the same. I use tests that way a lot; confirming I have not added a typo or simple syntax error. I take small steps so spotting the simple mistakes is... well simple.
The my $n = $ARGV[0] || shift; line is a way of making the sub handle input from the command line (the first arg, aka $ARGV[0]) or a scalar from the tests via shift.
In the above situation the test should fail as I am looking for "xxxxx", but I know that the code "should" return 1. So the test fails and tells me I am ok. SO then I can change the test to:
stdout_is { &run(5) }
'1',
'5 is a palindrome';
And this should pass, next we want to confirm the real behaviour, so test becomes:
stdout_is { &run(5) }
'1 as binary representation of 5 is 101 which is Palindrome.',
'5 is a palindrome';
This will fail of course, so lets change the script to pass this test (and forgive me I am skipping steps):
__PACKAGE__->run() unless caller;
use lib './lib';
use Binary::Palindrome;
sub run {
my $n = $ARGV[0] || shift;
my $bp = Binary::Palindrome->new;
if ( my $res = $bp->is_palindrome($n) ) {
print "$res as binary representation of $n is ",
$bp->represent_as_binary($n), " which is Palindrome.";
}
}
1;
This should pass, so lets add the next test:
stdout_is { &run(4) }
'0 as binary representation of 4 is 100 which is NOT Palindrome.',
'4 is NOT a palindrome';
Which fails, so then we add the code:
if ( my $res = $bp->is_palindrome($n) ) {
print "$res as binary representation of $n is ",
$bp->represent_as_binary($n), " which is Palindrome.";
}
else {
print "$res as binary representation of $n is ",
$bp->represent_as_binary($n), " which is NOT Palindrome.";
}
Adding this else makes our test passes. So we are in a good place.
This is a slightly contrived example, and there are many improvements to make. However, I now have a solid base to work from. This is a style of working I quite like. Think of it perhaps as Behaviour Driven Development (BDD) as well as TDD. o Or perhaps this could be described as an "Integration test" or even an "End to End test" given what we are building. I am not pedantic about the terms of purity of my TDD/BDD.
Personally I am aiming for tools that make my development easier and more reliable and this sort of testing does that for me.
When I coded this up I did TDD the module itself, so the two methods (is_palindrome and represent_as_binary) have tests.
Not doing the script and it's tests first, has affected the design of the module. The script is more complicated and the code less efficient as I a using both methods in the script AND I use represent_as_binary in the is_palindrome method. If it had side effects (i.e. wrote to a file or DB) then this would be bad; or if it was slow.
The next step might be to create a third method, that returns a data structure with the result and the binary representation. This would be more efficient; I would probably leave the construction of the text in the script as it's the "presentation layer" and leaves the module a little cleaner; though your mileage may vary. A method that returned the full answer would be very tidy in the script (thin controller).
Please do checkout the full code in the Perl Weekly Challenge git repo (https://github.com/manwar/perlweeklychallenge-club/tree/master/challenge-118/lance-wicks/perl).
And if you have any questions please drop me a message.