The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Test::Routine::Manual::Demo - a walkthrough, in code, of Test::Routine

VERSION

version 0.031

PERL VERSION

This module should work on any version of perl still receiving updates from the Perl 5 Porters. This means it should work on any version of perl released in the last two to three years. (That is, if the most recently released version is v5.40, then this module should work on both v5.40 and v5.38.)

Although it may work on older versions of perl, no guarantee is made that the minimum required version will not be increased. The version may be increased for any reason, and there is no promise that patches will be accepted to lower the minimum required perl.

The Demo

t/demo/01-demo.t

#!/bin/env perl
use strict;
use warnings;

# This test is both a test and an example of how Test::Routine works!  Welcome
# to t/01-demo.t, I will be your guide, rjbs.

{
  # This block defines the HashTester package.  It's a Test::Routine, meaning
  # it's a role.  We define state that the test will need to keep and any
  # requirements we might have.
  #
  # Before we can run this test, we'll need to compose the role into a class so
  # that we can make an instance.
  package HashTester;
  use Test::Routine;

  # We import stuff from Test::More because, well, who wants to re-write all
  # those really useful test routines that exist out there?  Maybe somebody,
  # but not me.
  use Test::More;

  # ...but then we use namespace::autoclean to get rid of the routines once
  # we've bound to them.  This is just standard Moose practice, anyway, right?
  use namespace::autoclean;

  # Finally, some state!  Every test will get called as method on an instance,
  # and it will have this attribute.  Here are some points of interest:
  #
  # - We're giving this attribute a builder, so it will try to get built with a
  #   call to $self->build_hash_to_test -- so each class that composes this
  #   role can provide means for these attributes (fixtures) to be generated as
  #   needed.
  #
  # - We are not adding "requires 'build_hash_to_test'", because then we can
  #   apply this role to Moose::Object and instantiate it with a given value
  #   in the constructor.  There will be an example of this below.  This lets
  #   us re-use these tests in many variations without having to write class
  #   after class.
  #
  # - We don't use lazy_build because it would create a clearer.  If someone
  #   then cleared our lazy_build fixture, it could not be re-built in the
  #   event that we'd gotten it explicitly from the constructor!
  #
  # Using Moose attributes for our state and fixtures allows us to get all of
  # their powerful behaviors like types, delegation, traits, and so on, and
  # allows us to decompose shared behavior into roles.
  #
  has hash_to_test => (
    is  => 'ro',
    isa => 'HashRef',
    builder => 'build_hash_to_test',
  );

  # Here, we're just declaring an actual test that we will run.  This sub will
  # get installed as a method with a name that won't get clobbered easily.  The
  # method will be found later by run_tests so we can find and execute all
  # tests on an instance.
  #
  # There is nothing magical about this method!  Calling this method is
  # performed in a Test::More subtest block.  A TAP plan can be issued with
  # "plan", and we can issue TODO or SKIP directives the same way.  There is
  # none of the return-to-skip magic that we find in Test::Class.
  #
  # The string after "test" is used as the method name -- which means we're
  # getting a method name with spaces in it.  This can be slightly problematic
  # if you try to use, say, ::, in a method name.  For the most part, it works
  # quite well -- but look at the next test for an example of how to give an
  # explicit description.
  test "only one key in hash" => sub {
    my ($self) = @_;

    my $hash = $self->hash_to_test;

    is(keys %$hash, 1, "we have one key in our test hash");
    is(2+2, 4, "universe still okay");
  };

  # The only thing of note here is that we're passing a hashref of extra args
  # to the test method constructor.  "desc" lets us set the test's description,
  # which is used in the test output, so we can avoid weird method names being
  # installed.  Also note that we order tests more or less by order of
  # definition, not by name or description.
  test second_test => { desc => "Test::Routine demo!" } => sub {
    pass("We're running this test second");
    pass("...notice that the subtest's label is the 'desc' above");
    pass("...and not the method name!");
  };
}

{
  # This package is one fixture against which we can run the HashTester
  # routine.  It has the only thing it needs:  a build_hash_to_test method.
  # Obviously real examples would have more to them than this.
  package ProcessHash;
  use Moose;
  with 'HashTester';

  use namespace::autoclean;

  sub build_hash_to_test { return { $$ => $^T } }
}

# Now we're into the body of the test program:  where tests actually get run.

# We use Test::Routine::Util to get its "run_tests" routine, which runs the
# tests on an instance, building it if needed.
use Test::Routine::Util;

# We use Test::More to get done_testing.  We don't assume that run_tests is the
# entire test, because that way we can (as we do here) run multiple test
# instances, and can intersperse other kinds of sanity checks amongst the
# Test::Routine-style tests.
use Test::More;

is(2+2, 4, "universe still makes sense") or BAIL_OUT("PANIC!");

# The first arg is a description for the subtest that will be run.  The second,
# here, is a class that will be instantiated and tested.
run_tests('ProcessHash class' => 'ProcessHash');

# Here, the second argument is an instance of a class to test.
run_tests('ProcessHash obj' => ProcessHash->new({ hash_to_test => { 1 => 0 }}));

# We could also just supply a class name and a set of args to pass to new.
# The below is very nearly equivalent to the above:
run_tests('ProcessHash new' => ProcessHash => { hash_to_test => { 1 => 0 }});

# ...and here, the second arg is not a class or instance at all, but the
# Test::Routine role (by name).  Since we know we can't instantiate a role,
# run_tests will try to compose it with Moose::Object.  Then the args are used
# as the args to ->new on the new class, as above.  This lets us write
# Test::Routines that can be tested with the right state to start with, or
# Test::Routines that need to be composed with testing fixture classes.
run_tests(
  'HashTester with given state',
  HashTester => {
    hash_to_test => { a => 1 },
  },
);

# There's one more interesting way to run out tests, but it's demonstrated in
# 02-simple.t instead of here.  Go check that out.

# ...and we're done!
done_testing;

t/demo/02-simple.t

# Welcome to part two of the Test::Routine demo.  This is showing how you can
# write quick one-off tests without having to write a bunch of .pm files or
# (worse?) embed packages in bare blocks in the odious way that 01-demo.t did.
#
# First off, we use Test::Routine.  As it did before, this turns the current
# package (main!) into a Test::Routine role.  It also has the pleasant
# side-effect of turning on strict and warnings.
use Test::Routine;

# Then we bring in the utils, because we'll want to run_tests later.
use Test::Routine::Util;

# And, finally, we bring in Test::More so that we can use test assertions, and
# namespace::autoclean to clean up after us.
use Test::More;
use namespace::autoclean;

# We're going to give our tests some state.  It's nothing special.
has counter => (
  is  => 'rw',
  isa => 'Int',
  default => 0,
);

# Then another boring but useful hunk of code: a method for our test routine.
sub counter_is_even {
  my ($self) = @_;
  return $self->counter % 2 == 0;
}

# Then we can write some tests, just like we did before.  Here, we're writing
# several tests, and they will be run in the order in which they were defined.
# You can see that they rely on the state being maintained.
test 'start even' => sub {
  my ($self) = @_;
  ok($self->counter_is_even, "we start with an even counter");

  $self->counter( $self->counter + 1);
};

test 'terminate odd' => sub {
  my ($self) = @_;

  ok(! $self->counter_is_even, "the counter is odd, so state was preserved");
  pass("for your information, the counter is " . $self->counter);
};

# Now we can run these tests just by saying "run_me" -- rather than expecting a
# class or role name, it uses the caller.  In this case, the calling package
# (main!) is a Test::Routine, so the runner composes it with Moose::Object,
# instantiating it, and running the tests on the instance.
run_me;

# Since each test run gets its own instance, we can run the test suite again,
# possibly to verify that the test suite is not destructive of some external
# state.
run_me("second run");

# And we can pass in args to use when constructing the object to be tested.
# Given the tests above, we can pick any starting value for "counter" that is
# even.
run_me({ counter => 192 });

# ...and we're done!
done_testing;

# More Test::Routine behavior is demonstrated in t/03-advice.t and t/04-misc.t
# Go have a look at those!

t/demo/03-advice.t

use Test::Routine;
use Test::Routine::Util;
use Test::More;

use namespace::autoclean;

# xUnit style testing has the idea of setup and teardown that happens around
# each test.  With Test::Routine, we assume that you will do most of this sort
# of thing in your BUILD, DEMOLISH, and attribute management.  Still, you can
# easily do setup and teardown by applying method modifiers to the "run_test"
# method, which your Test::Routine uses to run each test.  Here's a simple
# example.

# We have the same boring state that we saw before.  It's just an integer that
# is carried over between tests.
has counter => (
  is   => 'rw',
  isa  => 'Int',
  lazy => 1,
  default => 0,
  clearer => 'clear_counter',
);

# The first test changes the counter's value and leaves it changed.
test test_0 => sub {
  my ($self) = @_;

  is($self->counter, 0, 'start with counter = 0');
  $self->counter( $self->counter + 1);
  is($self->counter, 1, 'end with counter = 1');
};

# The second test assumes that the value is the default, again.  We want to
# make sure that before each test, the counter is reset, but we don't want to
# tear down and recreate the whole object, because it may have other, more
# expensive resources built.
test test_1 => sub {
  my ($self) = @_;

  is($self->counter, 0, 'counter is reset between tests');
};

# ...so we apply a "before" modifier to each test run, calling the clearer on
# the counter.  When next accessed, it will re-initialize to zero.  We could
# call any other code we want here, and we can compose numerous modifiers
# together onto run_test.
#
# If you want to clear *all* the object state between each test... you probably
# want to refactor.
before run_test => sub { $_[0]->clear_counter };

run_me;
done_testing;

t/demo/04-misc.t

use Test::Routine;
use Test::Routine::Util;
use Test::More;

use namespace::autoclean;

# One thing that the previous examples didn't show was how to mark tests as
# "skipped" or "todo."  Test::Routine makes -no- provisions for these
# directives.  Instead, it assumes you will use the entirely usable mechanisms
# provided by Test::More.

# This is a normal test.  It is neither skipped nor todo.
test boring_ordinary_tests => sub {
  pass("This is a plain old boring test that always passes.");
  pass("It's here just to remind you what they look like.");
};

# To skip a test, we just add a "skip_all" plan.  Because test methods get run
# in subtests, this skips the whole subtest, but nothing else.
test sample_skip_test => sub {
  plan skip_all => "these tests don't pass, for some reason";

  is(6, 9, "I don't mind.");
};

# To mark a test todo, we just set our local $TODO variable.  Because the test
# is its own block, this works just like it would in any other Test::More test.
test sample_todo_test => sub {
  local $TODO = 'demo of todo';

  is(2 + 2, 5, "we can bend the fabric of reality");
};

run_me;
done_testing;

t/demo/05-multiple.t

#!/bin/env perl
use strict;
use warnings;

use Test::Routine::Util;
use Test::More;

# One of the benefits of building our sets of tests into roles instead of
# classes is that we can re-use them in whatever combination we want.  We can
# break down sets of tests into bits that can be re-used in different cases.
# With classes, this would lead to multiple inheritance or other monstrosities.

# Here's a first Test::Routine.  We use it to make sure that one of our
# fixture's attributes is a numeric id.
{
  package Test::ThingHasID;
  use Test::Routine;
  use Test::More;

  requires 'id';

  test thing_has_numeric_id => sub {
    my ($self) = @_;

    my $id = $self->id;
    like($id, qr/\A[0-9]+\z/, "the thing's id is a string of ascii digits");
  };
}

# A second one ensures that the thing has an associated directory that
# looks like a unix path.
{
  package Test::HasDirectory;
  use Test::Routine;
  use Test::More;

  requires 'dir';

  test thing_has_unix_dir => sub {
    my ($self) = @_;

    my $dir = $self->dir;
    like($dir, qr{\A(?:/\w+)+/?\z}, "thing has a unix-like directory");
  };
}

# We might have one class that is only expected to pass one test:
{
  package JustHasID;
  use Moose;

  has id => (
    is      => 'ro',
    default => sub {
      my ($self) = @_;
      return Scalar::Util::refaddr($self);
    },
  );
}

# ...and another class that should pass both:
{
  package UnixUser;
  use Moose;

  has id  => (is => 'ro', default => 501);
  has dir => (is => 'ro', default => '/home/users/rjbs');
}

# So far, none of this is new, it's just a slightly different way of factoring
# things we've seen before.  In t/01-demo.t, we wrote distinct test roles and
# classes, and we made our class compose the role explicitly.  This can be
# a useful way to put these pieces together, but we also might want to write
# all these classes and roles as unconnected components and compose them only
# when we're ready to run our tests.  When we do that, we can tell run_tests
# what to put together.
#
# Here, we tell it that we can test JustHasID with Test::ThingHasID:
run_tests(
  "our JustHasID objects have ids",
  [ 'JustHasID', 'Test::ThingHasID' ],
);

# ...but we can run two test routines against our UnixUser class
run_tests(
  "unix users have dirs and ids",
  [ 'UnixUser', 'Test::ThingHasID', 'Test::HasDirectory' ],
);


# We can still use the "attributes to initialize an object," and when doing
# that it may be that we don't care to run all the otherwise applicable tests,
# because they're not interesting in the scenario we're creating.  For
# example...
run_tests(
  "a trailing slash is okay in a directory",
  [ 'UnixUser', 'Test::HasDirectory' ],
  { dir => '/home/meebo/' },
);

# ...and we're done!
done_testing;

AUTHOR

Ricardo Signes <cpan@semiotic.systems>

COPYRIGHT AND LICENSE

This software is copyright (c) 2010 by Ricardo Signes.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.