Strawberry
Luckily for us Perl developers, the team making Strawberry Perl have done an excellent job for quite some time. Their efforts make installing and using Perl on our Windows environments (PowerShell or cmd.exe) dead simple.
BerryBrew
That being said, something we find ourselves doing more and more frequently when doing development work on Linux is switching between versions of Perl quickly and easily. This has been available to us with BerryBrew for some time.
What Have I Done?! (PSPerl)
What I've started work on recently is a PowerShell version of a Perl environment switcher, called PSPerl. On linux we have perlbrew and plenv, why not have another option on Windows?
Given that it's written entirely in PowerShell v5+, it should be easy to update, maintain, and add new features - or even implement your own features. Give it a try. Find problems. Help me fix them.
The Good
It can be setup in an open PowerShell without need for elevated privileges. It can switch between Perl versions without needing to start a new shell or re-login. It can make your selected Perl version persistent, or you can just temporarily use it.
The Lacking...
I need to implement the ability to add local::lib libraries specific to your selected Perl version.
We need to be able to visit the strawberryperl site when behind a proxy.
We need to be able to execute commands against each installed Perl version.
Submit a Pull Request if you want to try your hand at fixing any of those lacking features.
]]>WWW::Shorten and friends came on my radar and I started there. I reached out to the authors of many modules in the WWW::Shorten to see if they wouldn't mind joining me in the effort to bring them all to a similar point. I seem to recall everyone being happy and eager to work together, so this part went well. We created a GitHub organization to house the various modules.
Inevitably, though, I began working on other things and unfortunately have let WWW::Shorten and friends languish. This, however, opens the opportunity for you to take on the task of maintaining a family of modules and bring their test coverage up to date and add in modules for the new shortening services that are available now.
Please let me know if you're interested and I'll happily pass off some commit and PAUSE permissions!
]]>Issue #186 has the work-around at the end. I hope that helps you.
]]>I haven’t been following the progress or reasoning behind Test2 and your blog post clearly laid it all out for me.
]]>A well-behaved web service gives you versioned endpoints to hit, a la Salesforce. If I write and test a client against version 34.0 of their services, I can only assume proper behavior against that version.
As new versions come up, I'm forced to read the change log and update my tests if need be.
]]>The hosted apps offer RESTful APIs for communication that you will need to use to transfer your data. Let's pretend that there isn't yet a Perl client implementation to fit our needs. So, the first thing that needs to be done is to write a client for these web services (using Mojolicious) to handle the few API methods you'll need.
You end up with an overly simplified client library that might look like this:
package WebService::Foo;
use Mojo::Base -base;
use Mojo::IOLoop;
use Mojo::URL;
use Mojo::UserAgent;
has service_url => sub { Mojo::URL->new('https://some_url/api/') };
has ua => sub { Mojo::UserAgent->new() };
sub account {
my ($self, $id, $cb) = @_;
my $url = Mojo::URL->new($self->service_url)->path("Account/$id");
#blocking
unless ($cb) {
my $tx = $self->ua->get($url);
die $tx->error->{message} unless $tx->success;
return $tx->res->json;
}
#non-blocking
Mojo::IOLoop->delay(
sub {
my $delay = shift;
$self->ua->get($url,$delay->begin);
},
sub {
my ($delay, $tx) = @_;
return $self->$cb($tx->error->{message}, undef) unless $tx->success;
return $self->$cb(undef,$tx->res->json);
}
)->catch(sub { $self->$cb(pop, undef) })->wait;
return $self;
}
1;
The client only implements one method, account
, that allows you to get accounts from the web service by their ID in either a blocking or a non-blocking way.
The usual list of statements as to why this type of testing is hard comes up quickly:
This inevitably leads to the following question: if I can't create accurate tests for it, how can I trust it in my application?
Well, in short, you mock it!
Using an application like Mojo's get command, cURL, or PostMan you can send erroneous and proper requests to the actual API service and copy the responses to create your mock.
The mock will end up being a simple web server that could look like this:
# some values
my $ID = '001W000000KY0vBIAT';
my $ID_DEL = '001W000000KY0vBIAC';
my $ID_BAD = '001W000000KY0vBZZZ';
my $RECORD = {Id => "001W000000KY0vBIAT",IsDeleted => 0,Name => 'foo'};
# setup mock
my $mock = Mojolicious->new;
$mock->routes->get('/api/Account/:id' => sub {
my $c = shift;
my $id = $c->stash('id');
return $c->render(status=>404,text=>"No ID supplied") unless $id;
return $c->render(status=>404,text=>"ID is deleted") if $id eq $ID_DEL;
return $c->render(status=>400,text=>"Malformed id") if $id eq $ID_MAL;
return $c->render(status=>200,json=>$RECORD) if $id eq $ID;
return $c->render(status=>404,text=>"Provided ID does not exist");
});
Now that you have your mock server, you can write a test for the account
method in our client above that might look like this:
use Mojo::Base -strict;
use Test::More;
use Mojolicious;
use Mojo::IOLoop;
use Try::Tiny qw(try catch);
BEGIN { use_ok('WebService::Foo') || BAIL_OUT("Can't use WebService::Foo"); }
my $ID = '001W000000KY0vBIAT';
my $ID_DEL = '001W000000KY0vBIAC';
my $ID_BAD = '001W000000KY0vBZZZ';
my $RECORD = {Id => "001W000000KY0vBIAT",IsDeleted => 0,Name => 'foo'};
my $ws = try { WebService::Foo->new(); } catch { BAIL_OUT("Unable to create new instance: $_"); };
isa_ok($ws, 'WebService::Foo', 'new: got an instance') || BAIL_OUT("can't instantiate");
# setup mock
my $mock = Mojolicious->new;
$mock->log->level('fatal'); # only log fatal errors to keep the server quiet
$mock->routes->get('/api/Account/:id' => sub {
my $c = shift;
my $id = $c->stash('id');
return $c->render(status=>404,text=>"No ID supplied") unless $id;
return $c->render(status=>404,text=>"ID is deleted") if $id eq $ID_DEL;
return $c->render(status=>400,text=>"Malformed id") if $id eq $ID_MAL;
return $c->render(status=>200,json=>$RECORD) if $id eq $ID;
return $c->render(status=>404,text=>"Provided ID does not exist");
});
$ws->ua->server->app($mock); # point our UserAgent to our new mock server
# set the URL
$ws->service_url(Mojo::URL->new('/api/'));
# actual testing
can_ok($ws, qw(account) );
# blocking tests
{
my $res;
# error handling
$res = try {return $ws->account() } catch { $_; };
like( $res, qr/No ID supplied/, 'account error: empty call');
$res = try {return $ws->account('') } catch { $_; };
like( $res, qr/No ID supplied/, 'account error: empty string call');
$res = try {return $ws->account(undef) } catch { $_; };
like( $res, qr/No ID supplied/, 'account error: undef call');
$res = try {return $ws->account($ID_DEL) } catch { $_; };
like( $res, qr/ID is deleted/, 'account error: deleted ID');
$res = try {return $ws->account($ID_MAL) } catch { $_; };
like( $res, qr/Malformed id/, 'account error: malformed ID');
$res = try {return $ws->account('gobblygook') } catch { $_; };
like( $res, qr/Provided ID does not exist/, 'account error: unknown id');
# successful tests
$res = try{return $ws->account($ID) } catch { $_; };
is_deeply( $res, $RECORD, 'account: successful result');
}
# non-blocking tests
{ # error: start and stop loop manually
my ($res, $err);
$ws->account($ID_MAL, sub {
(undef, $err, $res) = @_;
Mojo::IOLoop->stop;
});
Mojo::IOLoop->start;
like($err, qr/Malformed id/, 'account-nb: error: proper malformed ID error');
is($res, undef, "account-nb: error: proper undef results");
}
{ # success: start and stop loop manually
my ($res, $err);
$ws->account($ID, sub {
(undef, $err, $res) = @_;
Mojo::IOLoop->stop;
});
Mojo::IOLoop->start;
is($err, undef, 'account-nb: No errors');
is_deeply($res, $RECORD, "account-nb: successful result");
}
{ # error: delays are prettier
my ($res, $err);
Mojo::IOLoop->delay(
sub { $ws->account($ID_MAL, shift->begin()); },
sub { (undef, $err, $res) = @_; }
)->wait;
like($err, qr/Malformed id/, 'account-nb: error: proper malformed ID error');
is($res, undef, "account-nb: error: proper undef results");
}
{ # success: delays are prettier
my ($res, $err);
Mojo::IOLoop->delay(
sub { $ws->account($ID, shift->begin()); },
sub { (undef, $err, $res) = @_; }
)->wait;
is($err, undef, 'account-nb: No errors');
is_deeply($res, $RECORD, "account-nb: successful result");
}
done_testing;
The blocking tests are pretty straight-forward. If anything goes wrong, your client dies. You expect the caller to catch those errors using Try::Tiny. You test with bad IDs, deleted IDs, no IDs, good IDs, and you check the response from your mock.
The non-blocking tests are a bit less straight-forward. You can't be guaranteed the tests will run if they're done directly in the callback. So, you declare your error and response variables and update them when the callback gets executed.
You've got the same tests done in two ways above.
In the callback you manually stop the Mojo::IOLoop and then start it above where you want to run the tests.
my ($res, $err);
$ws->account($ID_MAL, sub {
(undef, $err, $res) = @_;
Mojo::IOLoop->stop;
});
Mojo::IOLoop->start;
like($err, qr/Malformed id/, 'account-nb: error: proper malformed ID error');
is($res, undef, "account-nb: error: proper undef results");
Another, cleaner (in my opinion) way to run the test would be to use the delay method of Mojo::IOLoop. This is much easier on the eyes and mind as you just have to wait on the steps to finish rather than worrying about when to start and stop the loop.
my ($res, $err);
Mojo::IOLoop->delay(
sub { $ws->account($ID_MAL, shift->begin()); },
sub { (undef, $err, $res) = @_; }
)->wait;
like($err, qr/Malformed id/, 'account-nb: error: proper malformed ID error');
is($res, undef, "account-nb: error: proper undef results");
If you've done your job well, your mock should behave how the real service behaves and give you some confidence that your client is written properly, allowing the caller to get data and trap errors.
]]>