DEV Community

loading...

TDD a perlscript with modulinos (PWC-118)

Lance Wicks
Judoka, Developer, Kiwi
Originally published at perl.kiwi on ・4 min read

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

So then we can expand the script to look like this:

__PACKAGE__ ->run() unless caller;

sub run {
    print "foo";
}

1;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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';

Enter fullscreen mode Exit fullscreen mode

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';

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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';

Enter fullscreen mode Exit fullscreen mode

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.";
    }

Enter fullscreen mode Exit fullscreen mode

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.

Tags: perlweeklychallengetdd

Discussion (0)