Test::...l::Demo(3pm)

πŸ’– NAME πŸ’–

Test::Routine::Manual::Demo β€” a walkthrough, in code, of Test::Routine πŸ¦‹

okay bestie, this is the User Contributed Perl Documentation page for Test::Routine::Manual::Demo(3pm). It's literally a whole tutorial in code, and we're about to read it together like a group chat. no cap.


✨ VERSION ✨

version 0.032 πŸ’…


πŸ¦‹ 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.

PERL VERSION vibes: it rides with the latest-ish perls, and the floor can move up whenever. periodt.

πŸ‘‘ 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) = @_;

okay so the counter saga continues and honestly? it ATE πŸ’… watch this: one test bumps the counter, and then we hit test test_1 => sub { ... } which just assumes it's back to the default again πŸ™ˆ

   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;

so the whole ✨vibe✨ here: instead of nuking the object and building a whole new one (which could have other, way more expensive resources already built 😭), we slap a before modifier onto run_test and call the clearer on the counter. next time something touches it, boom, it re-initializes to zero 🌸 and yes bestie, you can call literally any other code in there, and you can stack as many modifiers as you want onto run_test. also heads up: if you're out here wanting to clear every single bit of object state between each test... yeah you probably want to refactor 🫢


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

the tea β˜•: marking stuff as skipped or todo was never shown in the earlier examples, and the reason is kinda iconic β€” Test::Routine makes no provisions for those directives at all. it just trusts you to use the totally usable mechanisms that Test::More already gives you πŸ’–

so a normal test is just... a normal test, neither skipped nor todo, out here passing on purpose πŸŽ€

to skip one? you drop in a skip_all plan. and because test methods get run inside subtests, that skips the whole subtest and nothing else ✨

to mark one todo? you set your local $TODO variable. and because the test is its own block, it works exactly like it would in any other Test::More test πŸ¦‹ (yes, 2 + 2 is being compared to 5 in there, and yes, that's the point, we're bending the fabric of reality, let her cook 😭)


πŸ¦‹ 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;

okay so THIS one is the glow-up moment πŸ‘‘

the whole point of building your sets of tests as roles instead of classes is that you can re-use them in literally whatever combination you want 🫢 you get to break your tests down into little reusable bits and snap them together for different cases. with classes you'd be stuck with multiple inheritance or other monstrosities πŸ’€ no thank you.

first up, a Test::Routine that checks one of your fixture's attributes is a numeric id β€” it declares requires 'id'; and then has thing_has_numeric_id, matching the id against qr/\A[0-9]+\z/ and calling it "the thing's id is a string of ascii digits" ✨

second one makes sure the thing has an associated directory that looks like a unix path β€” requires 'dir';, test thing_has_unix_dir, matching against qr{\A(?:/\w+)+/?\z} with the description "thing has a unix-like directory" 🌸

then our classes: JustHasID is only expected to pass one test, and its id defaults to Scalar::Util::refaddr($self). and UnixUser should pass both β€” id defaults to 501, dir defaults to /home/users/rjbs πŸŽ€

and ngl, none of this is new information, it's just a slightly different way of factoring what we've already seen! in t/01-demo.t we wrote distinct test roles and classes and made the class compose the role explicitly. that's useful! but you might also want to write all these classes and roles as separate, unconnected components and only compose them when you're actually ready to run your tests πŸ’… and when you do that, you just tell run_tests what to put together:

  • run_tests("our JustHasID objects have ids", [ 'JustHasID', 'Test::ThingHasID' ]); β€” one class, one test routine 🌟
  • run_tests("unix users have dirs and ids", [ 'UnixUser', 'Test::ThingHasID', 'Test::HasDirectory' ]); β€” one class, two test routines πŸ”₯
  • run_tests("a trailing slash is okay in a directory", [ 'UnixUser', 'Test::HasDirectory' ], { dir => '/home/meebo/' },); β€” and you can still use the "attributes to initialize an object," and when you do that it might be that you don't care to run all the otherwise applicable tests, because they're not interesting in the scenario you're creating πŸ¦‹

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


🌸 AUTHOR 🌸

the icon, the legend, the whole entire vibe: Ricardo Signes πŸ’– hit him up at cpan@semiotic.systems 🫢


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

and fr fr, the legal bit, exactly as it is:

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

okay bestie that's the whole tour, we're at the footer, and the footer says the source is perl v5.40.1 and the date on the page is 2026-03-21 ⭐ so go write some Test::Routine roles and make your test suite as modular as your personality πŸ’…βœ¨ no cap, slay, periodt πŸŽ€πŸ’–πŸ¦‹

πŸ“¦ where this page comes from