The 2003 Perl Advent Calendar
[about] | [archives] | [contact] | [home]

On the 10th day of Advent my True Language brought to me..
Test::Inline

Hands up how many of you have uploaded modules to CPAN? Good. Hands up how many of you have made a stupid mistake in the module's documentation and had to upload another version? Too many of us it would seem.

I'm sad to say I've done this more than a couple of times. It's so easy to write a chunk of code directly into your pod and get lazy enough to never check that it compiles, or check that that does the right thing. Copying and pasting example code back and forth to your test suite is a tiresome task and it's easy to forget to do it. It's also entirely possible to document a feature that you never got round to writing tests for, and doesn't even work!

This is where Test::Inline comes in. It allows you to write tests directly in your pod. It can check if the code you've written compiles okay, so you can write a test for a feature as soon as you've written in the documentation that your code can do something. It also allows you to markup the code examples you're including in your documentation to indicate that the testing routines should check that they compile and that they produce the correct output.

All these tests can be extracted with a utility program that creates a test script that ships with your code, meaning you never need have to manage two separate files again. How handy.

Let's write a simple module to get the modules published in the Perl advent calendar.

  package WWW::PerlAdventCalendar;
  # turn on perl's safety features
  use strict;
  # declare a version
  use vars qw($VERSION);
  $VERSION = 0.01;
  use LWP::Simple qw(get);
  use XML::RSS;
  =head1 NAME
  WWW::PerlAdventCalendar - get module for the day
  =head1 SYNOPSIS
    use WWW::PerlAdventCalendar;
    my $advent = WWW::PerlAdventCalendar->new();
    my $module = $advent->module_for(2002,3);
  =head1 DESCRIPTION
  A simple interface that works out what modules have been
  published on the Perl advent calendar for a year.  This module
  uses the XML RDF streams that.
  Using it is simple.  All you need do is create the object,
  and then ask the object what the module is for a year and date.
    # print out what module was on the 7th December 2003
    my $advent = WWW::PerlAdventCalendar->new();
    print $advent->module_for(2003,7);
  Each RSS feed is downloaded once (and only once) on demand and cached.
  To clear a year's cache, all you have to do is call the clear_cache
  method with the year in question
    $advent->clear_cache(2003);
  =cut
  # basic standard constructor
  sub new
  {
    my $class = shift;
    return bless {}, $class
  }
  # simply look up and return the correct module, downloading
  # and parsing the year's rss if we need to
  sub module_for
  {
    my $self = shift;
    my $year = shift;
    my $day  = shift;
    # get the year if we haven't got it already
    unless ($self->{cache}{ $year })
      { $self->_parse_year($year) };
    # return the module for that day
    return $self->{cache}{ $year }[ $day ]
  }
  # download and parse year
  sub _parse_year
  {
    my $self = shift;
    my $year = shift;
    # get the year
    my $xml = get "http://perladvent.org/perladvent${year}.rdf"
      or die "Can't get year '$year'";
    # parse it into a rss file
    my $rss = XML::RSS->new();
    $rss->parse($xml);
    # fill up the year
    my $count = 1;
    for my $item (reverse @{ $rss->{items} })
    {
      # extract out the module name
      ($self->{cache}{ $year }[ $count++ ]) =
        $item->{title} =~ /\d+[a-z]+ - (.*)$/;
    }
  }
  sub clear_cache
  {
    my $self = shift;
    my $year = shift;
    delete $self->{cache}{ $year };
  }
  =head1 AUTHOR
  Written by Mark Fowler E<lt>mark@twoshortplanks.comE<gt>
  Copyright Mark Fowler 2003.  All Rights Reserved.
  This program is free software; you can redistribute it
  and/or modify it under the same terms as Perl itself.
  =head1 BUGS
  None known.
  =head1 SEE ALSO
  L<XML::RSS>, L<LWP::Simple>
  =cut
  1;

You can see how there's several code snippets talked about in the documentation that I'd like to check actually function as I expect them to. What we need to do is markup some of the text using extra notation so that Test::Inline knows what to extract. We start with we markup the text in our synopsis:

  =head1 SYNOPSIS
  =for example begin
    use WWW::PerlAdventCalendar;
    my $advent = WWW::PerlAdventCalendar->new();
    my $module = $advent->module_for(2002,3);
  =for example end
  =head1 DESCRIPTION

It's important to include a blank line after each of the =for directives. This pod will be rendered by perldoc (and pod2html and all the other pod viewers) no differently to the previous version; however we can now use the pod2test to extract these tests and install them into a file:

  bask-2.05b$ mkdir t
  bash-2.05b$ pod2test lib/WWW/PerlAdventCalendar.pm  > t/01basic.t

This test file can be then run like any other test. The amazing thing is that because the outputted test script doesn't rely on Test::Inline you don't even need to get your end users to install it. You only need the module installed in order to extract the test from the pod.

When we run the test we get the rather satisfactory result indicating that our code compiled okay and didn't contain any syntax errors.

  bash-2.05b$ perl -Ilib t/01basic.t 
  ok 1 - example from line 20
  1..1

If we want to test the code any closer, to make sure the code actually did the job it was supposed to we need to add extra test code that isn't printed out when the pod is viewed through a normal pod renderer. We do this with the =for example_testing notation:

  =head1 SYNOPSIS
  =for example begin
    use WWW::PerlAdventCalendar;
    my $advent = WWW::PerlAdventCalendar->new();
    my $module = $advent->module_for(2002,3);
  =for example end
  =for example_testing
    is($module,"DBD::SQLite","3rd dec 2002 is DBD::SQLite");
  =head1 DESCRIPTION

All the code we want to run to check must immediately follow the =for statement and you can't leave any blank lines - leaving blank lines would indicate to pod that the current =for example_testing has ended and the rest of the code should be treated as normal pod and rendered rather than converted into tests. Anyway, it all works:

  bash-2.05b$ perl -Ilib t/01basic.t 
  ok 1 - example from line 20
  ok 2 - 3rd dec 2002 is DBD::SQLite
  1..2

The second code example we want to test is about half way down the page. This time we want to check it compiles and prints out the correct thing:

  =for example begin
    # print out what module was on the 7th December 2003
    my $advent = WWW::PerlAdventCalendar->new();
    print $advent->module_for(2003,7);
  =for example end
  =for example_testing
    is($_STDOUT_,"Attribute::Handlers", "prints out A::H");

All the output that is printed to STDOUT is captured by the code Test::Inline produces and is stored in $_STDOUT_ ($_STDERR_ works similarly) enabling you to run tests against it.

The final chunk of code in our pod needs setup before it's compiled as it's just a simple one line example that assumes $advent has already been created. If we're going to test that the cache is cleared like it says, we need to create $advent and download a page before we run the example code:

  =for example
    my $advent = WWW::PerlAdventCalendar->new();
    $advent->module_for(2003,3);
  =for example begin
    $advent->clear_cache(2003);
  =for example end
  =for example_testing
    ok(!$advent->{cache}{2003},"cache empty");

And that's the examples in the code tested. It's still probably a good idea to write an extra test suite to cover the cases that haven't been covered here (for example, does the code work if we clear a cache that doesn't exist? Does it throw an error correctly in getting an erroneous year?) However, all the examples we've written in the pod work and we can be assured anyone using the documentation will have a solid basis to build their code on.

  • Test::Inline::Tutorial
  • Test::More
  • LWP::Simple
  • XML::RSS