Hello dear esteemed monks,
More than once I felt an urge to insert a set of Test::More's checks into my application code, for instance when loading a plug-in or validating a complex piece of data. However, Test::More/Test::Builder is best suited to only run inside test scripts.
So I came up with a module to fill the gap. And I'm going to release it to CPAN soon, unless some huge problem is detected.
The idea is as follows:
- A contract is an object representing a series of checks/assertions. It has some control methods as well as the checks themselves. A counterpart method exists for every of Test::More's checks.
- The most basic check is refute($condition, $message) which may be viewed as an inverted ok(). It is assumed that a passing test does not need attention, while a failing one begs for an explanation.
- New checks may be added quite easily, appearing as both contract's methods and individual functions that run just fine under Test::More.
- And these checks can be tested (who shaves the barber?) fairly easily using the contract_is ($contract, "11000101") assertion.
- Prototyped exception-proof contract { BLOCK; } sugar exists to reduce the bolierplate. Such blocks may be nested.
What usage I can see this far:
- loading user-supplied code;
- validating complex pieces of data;
- checking that multiple implementations (XS vs PP, different backends etc) behave exactly the same;
- maybe some use exists for development using traits/mixins (aka Moose::Role);
- maybe some assertion modules may be armed with a refute($what_went_wrong, $why_we_care) call.
Some details (this is basically a copy-and-paste of the project's README):
IN-APP CHECKS
The following is going to perform some checks and output an explanation (similar to a test script output) if something went wrong.use strict; use warnings; use Test::Contract; my $c = contract { $_[0]->like( $user_input, qr/.../, "Format as expected" ); $_[0]->isa_ok( $some_object, "Some::Class" ); }; if ($c->get_passing) { # so far, so good - move on! } else { croak "Contract failed: ".$c->get_tap; };
Of course, Test::Contract can be instantiated just fine if one needs more fine-grained control.
EXTENDING THE ARSENAL
As said above, the most basic check in Test::Contract is $contract->refute( $what_went_unexpected, $why_we_care_about_it );. This may be viewed as an inverted ok or assert:
sub refute { my ($condition, $message) = @_; ok (!$condition, $message) or diag $condition; };
So all one needs to build a new check is to create a function that returns false when its arguments are fine, and an explanation of failure when they are not. Think pure function, although it may have side effects, e.g. checking that a file exists.
A Test::Contract::Engine::Build module exists to simplify the task further:
package My::Check; use Exporter qw(import); use Test::Contract::Engine::Build; build_refute my_check => sub { my ($got, $expected) = @_; # ... a big and nasty check here }, args => 2, export => 1; 1;
This would create an exported function called my_check in My::Check, as well as a my_check method in Test::Contract itself. So the following code is going to be correct:
use Test::More tests => 1; use My::Check; my_check $foo, $bar, "foo is fine";
And this one, too:
# inside a running application use Test::Contract; use My::Check(); # don't pollute global namespace my $c = Test::Contract->new; $c->my_check( $foo, $bar, "runtime-generated foo is fine, too" ); if (!$c->get_passing) { # ouch, something went wrong with $foo and $bar };
It is also possible to validate the testing module itself, outputting details on specifically the tests with unexpected results:
use Test::More; use Test::Contract::Unit qw(contract_is); use My::Check; my $c = contract { my_check $proper_foo, $bar; my_check $good_foo, $bar; my_check $broken_foo, $bar; my_check $good_foo, $wrong_bar; }; is_contract $c, "1100", "my_check works as expected"; done_testing;
SOME PHILOSOPHY
Using refutation instead of assertion is similar to the falsifiability concept in modern science.
Or, quoting Leo Tolstoy, "All happy families are alike; each unhappy family is unhappy in its own way".
I would be quite happy if this concept goes outside the Perl community.
CONCLUSION
As stated above, this module is going to be released to CPAN, but maybe I'm missing something very obvious here...
My previous submission on the topic: RFC: Test::Refute - extensible unified assertion & testing tool.
The project link again: https://github.com/dallaylaen/perl-test-contract.
Thank you, and hope you enjoyed the reading!
|
---|
Replies are listed 'Best First'. | |
---|---|
Re: RFC: Test::Contract - extensible object-oriented runtime check series
by tobyink (Canon) on May 08, 2017 at 01:32 UTC | |
by Dallaylaen (Chaplain) on May 08, 2017 at 09:56 UTC | |
by tobyink (Canon) on May 08, 2017 at 13:27 UTC | |
by Dallaylaen (Chaplain) on May 11, 2017 at 04:46 UTC | |
Re: RFC: Test::Contract - extensible object-oriented runtime check series
by Dallaylaen (Chaplain) on Dec 31, 2017 at 11:19 UTC | |
Re: RFC: Test::Contract - extensible object-oriented runtime check series (DbC)
by Arunbear (Prior) on Dec 26, 2017 at 13:35 UTC | |
by Dallaylaen (Chaplain) on Dec 26, 2017 at 16:49 UTC |