Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Keeping your objects healthy: Object::Exercise

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

Keeping your objects healthy: Object::Exercise

Unit testing is frequently repetitive: pick a subname, pass some
arguments, compare a result. Object::Excercise takes the lather,
rinse, repeat out of the coding by providing a simple data
structure to dispatch code with pre-arranged arguments and
validate the results. This talk looks at the underlying dispatch
mechanism in Perl that allows this and ways of using the module
to test a variety of code.

Avatar for Steven Lembark

Steven Lembark PRO

April 14, 2026

More Decks by Steven Lembark

Other Decks in Technology

Transcript

  1. Recall that Lazy-ness is a virtue Hardcoded testing is a

    pain. Tests require tests... Alternative: Data-driven testing. Declarative specs. Generated tests.
  2. We have all written this: Create an object. Run a

    command. Check $@. Execute a test. Run a command...
  3. The test cycle my $obj = $class->new( @argz ); eval

    { my $expect = { qw( your structure from hell ) }; my $found = $obj->foo( @foo_argz ); cmp_deeply $found, $expect, "foo"; }; BAIL_OUT "foo fails" if $@; eval { my $expect = [ { qw( another structure from hell ) } ]; my $found = [ $obj->bar( @bar_argz ) ]; cmp_deeply $found, $expect, "bar"; }; BAIL_OUT "bar fails" if $@; ...
  4. MJD's “Red Flags” Code is framework, probably updated by cut+paste.

    Spend more time hacking framework than module. Hardwiring the the tests requres testing the tests. Troubleshooting the tests... Updating the tests.
  5. Fix: Add a level of abstraction. Put loops, boilerplate into

    framework. Data drives test loop. Metadata used to generate test data. Replace hardwired tests with data.
  6. Abstraction: Object::Exercise A “little language”. Describe a call, sanity checks.

    Data drives the tests. Array based for simpler processing.
  7. Perl makes this easy Perl can dispatch $object->$method( ... ).

    The $method scalar can be Text for symbol table dispatch. Subref for direct dispatch into code. Text for whitebox, Subref for blackbox.
  8. Planning your exercise. An array of steps. Each step is

    an operation or directive. Directives are text like “verbose”. Operations are arrayref's with a Method + args. Expect & outcome.
  9. Entering the labrynth Entry point is a subref. Avoids namespace

    collisions with methods: $object->$exercise( @testz ); initiates testing.
  10. Perly One-Step Nothing more than a method and arguments: [

    method => arg, ... ] Executes $obj->$method( arg, … ). Successful if no exceptions.
  11. Testy Two-Step Supply fixed return as arrayref: [ [ method

    => arg, ... ], [ compare values ], ] [ 1 ], [ undef ], [ %struct_from_hell ] Compared to [ $obj->$method( @argz ) ].
  12. Expecting failure Testing failure modes raises exceptions. Explicit undef expects

    eval { ... } to fail. Reports successful exception: [ [ method => @bogus_values ], undef ]
  13. Saying it your way Third element is a message: [

    [ $method, @blah_blah ], [ 42 ], “$method supplies The Answer” ]
  14. Great expectations Beyond fixed values: regexen, subrefs. qr/ ... /

    $found->[0] =~ $regex ? pass : fail ; sub { ... } $sub->( @$found ) ? pass : fail ; Generated regexen, closures simplify testing. Both can have the optional message.
  15. Giving orders Directives are text scalars. Turn on/off verbosity in

    tests. Set a breakpoint before calling $method. Treat input as regex text and compile it.
  16. One-test settings Text scalars before first arrayref are directives. Test

    frobnicate verbosely, rest no-verbose: noverbose => [ verbose => [ qw( frobnicate blah blah ) ] ],
  17. One-test breakpoint Adds $DB::Single = 1 prior to calling $obj->$method.

    Simplifies perl -d to check a single method. [ debug => [ qw( foobar bletch blort ) ], [ 1 ] ]
  18. Regexen in YAML::XS YAML::XS segfaults handling stored rexen. “regex” directive

    compiles expect value as regex: [ regex => [ qw( foobar bletch) ], '[A-Z]\w+$', ] $obj->foobar( 'bletch' ) =~ qr/[A-Z]\w+$/;
  19. Result: Declariative Testing Tests can be stored in YAML, JSON.

    Templates can be expanded in code. Text methods useful for “blackbox” testing. Validate what method returns. Coderef's useful for “whitebox” testing. Investigate internal structure of object. Reset incremental values for iterative tests.
  20. Example: Adventure Map Test Minimal map: One locations entry. name:

    Empty Map 00 namespace : EmptyMap00 locations: blackhole: name: BlackHole description : You are standing in a Black Hole. exits : Out : blackhole
  21. Override the map contents One way: Store separate mission files.

    Every test needs to be replicated. Two way: Replace the map. Override the method returning locations.
  22. Init reads the map "add_locations" processes the map: sub init

    { my ($class, $config_path) = @_; die "You must specify a config file to init()" unless defined $config_path; $_config = YAML::LoadFile($config_path); $class->add_items($_config->{items}); $class->add_actors($_config->{actors}); $class->add_locations($_config->{locations}); $class->add_player('player1', $_config->{player}); }
  23. Adding locations is a loop They are added one-by-one. sub

    add_locations { my ($class, $locations) = @_; foreach my $key (keys %{$locations}) { $class->add_location($key, $locations->{$key}); } }
  24. Adding one location Finally: The root of all evil... New

    locations are added with a key and struct: sub add_location { my ($class, $key, $config) = @_; my $location = Adventure::Location->new; $location->init($key, $config); $class->locations->{$key} = $location; }
  25. Faking the location sub install_test_location { my $package = shift;

    my $test_struct = shift; *{ qualify_to_ref add_location => $package } = sub { my ($class, $key ) = @_; $class->locations->{$key} = Adventure::Location ->new ->init($key, $test_struct ); # hardwired map }; }
  26. Aside: Generic “add” handler # e.g., install Adventure::Location::add_location sub add_thingy

    { my ( $parent, $thing ) = @_; my $name = ‘add_’ . $thing; my $package = qualify ucfirst(lc $thing), $parent; *{ qualify_to_ref $name => $package } = sub { my ($class, $name, $data ) = @_; $class->$type->{$name} = $package->new->init($name, $data ); };