Beefy Boxes and Bandwidth Generously Provided by pair Networks
Keep It Simple, Stupid
 
PerlMonks  

RFC: Test::Contract - extensible object-oriented runtime check series

by Dallaylaen (Chaplain)
on May 07, 2017 at 14:17 UTC ( [id://1189735]=perlmeditation: print w/replies, xml ) Need Help??

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

    Have you looked at the various implementations of the assert keyword on CPAN?

      There are a lot of them; could you please specify the most prominent?

      As for the modules I've managed to look into, for instance Carp::Assert, they seem to focus on optimizing themselves away in production.

      I think this could be a valuable future option for my module. However, this is not the main point of it.

      For now, it's more about providing a way to actually impose limitations on the way code works in both development and production. Optimisation is for later...

        I guess I'm not really sure what your use case is.

        If your code is mostly object-oriented, you can take advantage of type constraints in any of the major OO frameworks. That automatically covers you for object constructors and accessors.

        Then if you want to check the parameters passed to method calls and function calls, you can re-use the same type constraint checking, courtesy of things like Type::Params. For example, if you want to check that your add function has been passed two numbers:

        use feature qw(state); # Perl 5.10+ use Type::Params qw(compile); use Types::Standard qw(Num); sub add { state $check = compile( Num, Num ); my ($x, $y) = $check->(@_); # will croak if not passed two numbers my $sum = $x + $y; return $sum; }

        If you want to check that certain variables are sane within your function, not just the parameters that are passed as input, then you can still do this with type constraints:

        use feature qw(state); # Perl 5.10+ use Type::Params qw(compile); use Types::Standard qw(Num); sub add { state $check = compile( Num, Num ); my ($x, $y) = $check->(@_); # will croak if not passed two numbers my $sum = $x + $y; Num->assert_return( $sum ); }

        Though frankly, if you keep your function definitions short and simple, this is rarely necessary.

        Then you just come down to a handful of sanity checks that go beyond what type constraints would normally do.

        use feature qw(state); # Perl 5.10+ use Type::Params qw(compile); use Types::Standard qw(ArrayRef); use Carp; use PerlX::Assert; sub zip { state $check = compile( ArrayRef, ArrayRef ); my ($x, $y) = $check->(@_); # An additional check on input. # Carp is a pretty appropriate way of dealing with this. # croak "Arrays must be same length" unless @$x == @$y; my @z = map { [$x->[$i], $y->[$i]] } 0 .. $#$x; # These assertions check the internal logic of our # function, so using Carp makes less sense. It's not # the caller's fault if they fail. Here an assertion # feature makes sense. And because we believe # our logic to be correct, it's hopefully okay to # optimize this away in the production environment. # assert '@z has same length as @$x' { @z == @$x }; assert '@z has same length as @$y' { @z == @$y }; # \@z should be an arrayref of arrayrefs. # ArrayRef->of(ArrayRef)->assert_valid( \@z ); }

        I don't see what the point of adding a big Test::Builder-like framework on top of this would be. What can it cover which the above cannot?

Re: RFC: Test::Contract - extensible object-oriented runtime check series
by Dallaylaen (Chaplain) on Dec 31, 2017 at 11:19 UTC
    The module has been released under a much less ambitious name Assert::Refute.
    • Added configurable runtime assertions via refute_these{ # ok, like here }; # warns if not pass;
    • Separated contract definitions from contract reports;
    • Builder builds subs that run just fine both as runtime assertions and under Test::More;
    • Own unit-testing engine was withheld.
Re: RFC: Test::Contract - extensible object-oriented runtime check series (DbC)
by Arunbear (Prior) on Dec 26, 2017 at 13:35 UTC
    Hi, did you know that there's already a long established notion of a software contract? I.e. Design by contract (or DbC).

    With DbC, contracts are actually part of the code, not secondary constructs like unit tests. Contracts and unit tests are different, 'orthogonal' ways of verifying code. Contracts can be enabled/disabled at run time, and you wouldn't test the contract as such, though you could have tests that exercise typical workflows and contract violations would show up as exceptions being raised.

    Also contracts in this sense are not meant to validate data, but to verify an object API is working as agreed.

    Some modules that support DbC: Class::Contract, MooseX::Contract.

      Hello Arunbear,

      I probably didn't at the time of the grandparent post, but I did look it up since and abandoned contract in the name for good.

      I don't think what I want is full-fledged DbC though. It's a bit "take it or leave it" - adding it to just some subs looks silly and adding it throughout an (already working) project seems like a bit of overkill and also a big and risky task.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://1189735]
Approved by haukex
Front-paged by haukex
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others lurking in the Monastery: (4)
As of 2024-03-28 13:48 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found