Pearl Modolino’s Devilish (Test) joke

In the last two years, I’ve been playing Dungeons & Dragons, the famous table top fantasy role-playing game. As a software developer and musician, one of my favorite character classes to play is Bard, a magical and inspiring actor or word maker. The list of basic Bardik mantras includes satirical, seductive verbal barbs that have the power to psychologically harm and harm an opponent. Even if they do not understand the words.. (Can you see why this one coder is so attractive?)

Fun software also has a role to play in testing, in the form of fictitious objects that mimic parts of a system that are too fragile, too slow, too complex, or too difficult to actually use. They enable discrete unit testing without relying on external dependencies of the code being tested. Moxes are great for databases, web services, or other network resources where the goal is to get what you write, not what is in the “cloud.”

Speaking of web services and jokes, one of my favorites is the long-running FOAAS (the language in the link is not safe for work), a surprisingly extensive restful insult service. Of course there is a related Perl client API, but what I didn’t remember was a simple Perl script to call this API from the terminal command line. So I wrote the following on Thanksgiving break, trying to keep it simple and also show the basics of making fun of such an API. It features some new Pearl syntax and testing techniques, as well as Brian de Foy’s module concept. Mastering Pearl (Second Edition, 2014) Which converts scripts and modules into self-contained executable libraries.

#!/usr/bin/env perl

package Local::CallFOAAS;  # this is a modulino
use Test2::V0;             # enables strict, warnings, utf8

# declare all the new stuff we're using
use feature qw(say state);
use experimental qw(isa postderef signatures);
use Feature::Compat::Try;
use Syntax::Construct qw(non-destructive-substitution);

use WebService::FOAAS ();
use Package::Stash;
use Exception::Class (
    NoMethodException => {
        alias  => 'throw_no_method',
        fields => 'method',
    },
    ServiceException => { alias => 'throw_service' },
);

my $foaas = Package::Stash->new('WebService::FOAAS');

my $run_as =
    !!$ENV{CPANTEST}       ? 'test'
  : !defined scalar caller ? 'run'
  :                          undef;
__PACKAGE__->$run_as(@ARGV) if defined $run_as;

sub run ( $class, @args ) {
    try { say $class->call_method(@args) }
    catch ($e) {
        die 'No method ', $e->method, "n"
          if $e isa NoMethodException;
        die 'Service error: ', $e->error, "n"
          if $e isa ServiceException;
        die "$en";
    }
    return;
}

# Utilities

sub methods ($) {
    state @methods = sort map s/^foaas_(.+)/$1/r,
      grep /^foaas_/, $foaas->list_all_symbols('CODE');
    return @methods;
}

sub call_method ( $class, $method = '', @args ) {
    state %methods = map { $_ => 1 } $class->methods();
    throw_no_method( method => $method )
      unless $methods{$method};
    return do {
        try { $foaas->get_symbol("&$method")->(@args) }
        catch ($e) { throw_service( error => $e ) }
    };
}

# Testing

sub test ( $class, @ ) {
    state $stash = Package::Stash->new($class);
    state @tests = sort grep /^_test_/,
      $stash->list_all_symbols('CODE');

    for my $test (@tests) {
        subtest $test => sub {
            try { $class->$test() }
            catch ($e) { diag $e }
        };
    }
    done_testing();
    return;
}

sub _test_can ($class) {
    state @subs = qw(run call_method methods test);
    can_ok( $class, @subs, "can do: @subs" );
    return;
}

sub _test_methods ($class) {
    my $mock = mock 'WebService::FOAAS' => ( track => 1 );

    for my $method ( $class->methods() ) {
        $mock->override( $method => 1 );

        ok lives { $class->call_method($method) },
          "$method lives";
        ok scalar $mock->sub_tracking->{$method}->@*,
          "$method called";
    }
    return;
}

sub _test_service_failure ($class) {
    my $mock = mock 'WebService::FOAAS';

    for my $method ( $class->methods() ) {
        $mock->override( $method => sub { die 'mocked' } );

        my $exception =
          dies { $class->call_method($method) };
        isa_ok $exception, ['ServiceException'],
          "$method throws ServiceException on failure";
        like $exception->error, qr/^mocked/,
          "correct error in $method exception";
    }
    return;
}

1;

Let’s go through the code above.

beginner

First of all, there is a general shingling line to point out that the Unix and Linux systems should be used. perl Is found in the user’s PATH through. env Command. I announce the package name (in the local :: name space) so that the default is not contaminated. main A package of other scripts you might want. require This as a module. Then I use Test2 :: V0 bundle from Test2 :: Suite because embedded testing code uses many of its functions. It also has the side effect of stiffening, alerting, and activating the utf8 process, so there is no need to explicitly do so. use She’s here.

(Test :: More and why Test2 instead of its derivatives and add-ons? Both are retained by the same author, who suggested earlier. I see it using more and more modules, so I Thought it would be a great opportunity to learn.)

I will then announce all the new features of Perl that I would like to use that clearly need to be enabled so as not to sacrifice backward compatibility with older versions of Perl 5. As of this writing, some of these features ( isa Class instance operator, named subroutine signature, and try/catch Exception handling syntax) is considered. experimentalIn older versions of Perl with the Enable Feature :: Compat :: Try module. Friendly postfix deferencing syntax was mainlined in Perl version 5.24, but versions 5.20 and 5.22 still require experimentation. Finally, I use Syntax::Construct To announce /r Flags for non-destructive regular expression text substitutes introduced in version 5.14.

Next, I bring the above mentioned FOAAS Perl API without importing any of its functions, Package :: Stash to simplify the metaprogramming, and some exceptional classes so that the command line function and other users can better understand that. What is the cause of failure? In the preparation of the following methods to actively discover which functions are provided by WebService :: FOAAS, its symbol table (or stash). $foaas Variable

The next block determines how, if anything, I am going to run the code as a script. If CPANTEST Climate variable is set, I’ll call test Class method subBut if there is no sub-routine to call me, I will follow it. run Class method. You will receive command line arguments from either. @ARGV. If none of these conditions are correct, do nothing. The rest of the code method is announced.

Modulino methods, metaprogramming, and exceptions

This is the first of them. run The method is a thin sheet around it. call_method The class method is detailed below, either to draw a conclusion or dieIng with a reasonable error depending on the class of the discount thrown. Although I chose not to write tests for this output, future tests can call this method and reclaim these exceptions to match against them. Messages end with a. n New line character then die The current script does not know how to add line number.

The next is the utility method called. methods Which uses Package :: Stash’s to retrieve all designated names. CODE Blocks (ie, subs) WebService :: From the FOAAS symbol table. Reading from right to left, then filtered with them. grep Just to find people who are just starting out. foaas_ And then change with map To remove this precedent. The list goes on. sortEd and save in one state Variable and back so it doesn’t need to be restarted.

(As a side note, though perlcritic Strictly speaking, I have chosen the forms of expression. grep And map Here’s their block form for simplicity. It’s okay to break the rules if you have a good reason.)

sub call_method This is where the real action takes place. Its parameters are the class it is called, FOAAS name $method (Default on an empty string), and in a row of optional arguments @args. I already create a hash or associative array. methods The method I use again to see if the name of the passed method is the same as the one I know about. If not, I’ll throw it away. NoMethodException Using throw_no_method Alias ​​function was created when I used Exception :: Class in the beginning. Using a function instead NoMethodException->throw() This means that it is checked at compile time instead of runtime, to catch typing errors.

I get all the routines (indicated by a & Attributed to $method From $foaas Pass the stash and more arguments received. @args. If that WebService :: FOAAS sub-routine makes an exception, it will be caught and thrown away again. ServiceException; In other case call_method Returns the result. It is up to the caller to decide what to do with this result or any exceptions, if any.

Testing Modulino with Max

This is where I start using the Test2 :: Suite tools I mentioned earlier. Of test The class method begins with making a filtered list of all. subStarts with _test_ In the current class, very much like. methods Top up with WebService :: FOAAS. I then loop through this list subs, running each as an. subtest Contains class method with any exceptions reported as evaluation.

The rest are modulino sub test methods, starting with a simple one. _test_can IQ test for public practice in the classroom. After that _test_methodsI want any add-ons, overrides, or sets that start with ing the WebService :: FOAAS package and tell Test2 :: Mock subs Then I looped all the procedure names methods Class method, to give each one a simple real value return. I then check to pass these names. call_method And use the return hash reference to check that the ride has been overridden. sub Was called. It sounds a lot easier than the Test :: Builder based fun libraries I’ve tried like Test :: MockModule and Test :: MockObject.

_test_service_failure That’s pretty much the way it works, check it out call_method Throws correctly ServiceExceptions if wrapped WebService :: FOAAS function dies main difference is that mocked WebService :: FOAAS subs has now been overridden with a code reference (sub { die 'mocked' }), which one call_method Uses to settle rethrown. ServiceExceptionOf error Playground

Finish it

Fortunately, this article gives you some ideas, whether it’s making scripts (maybe legacy code) testable to improve them, or writing better unit tests that make fun of dependencies, or a little bit of meta programming. Adapt so you can dynamically support and test new features. Said dependent. I hope you are not angry at least. Let me know in the comments what you think.

.

Write a Comment