The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Assert::Refute - Unified testing and assertion tool

DESCRIPTION

This module adds Test::More-like code snippets to your production code, without turning the whole application into a giant testing script.

This can be though of as a lightweight design-by-contract form.

New testing conditions may be added quite easily, working exactly the same in both production environment and test scripts.

SYNOPSIS

The following code will issue a warning if required conditions are not met:

    use Assert::Refute qw( :all );

    my ($foo, $bar, $baz);

    # .......
    # Big and hard to test chunk of code here
    # .......

    refute_these {
        like $foo, qr/f?o?r?m?a?t/, "Format as expected";
        can_ok $bar, qw( do_this do_that frobnicate ),
            "Duck-typing an object";
        cmp_ok $baz, ">", 0, "baz is positive";
    };

As the chunk-of-code is being rewritten into a proper function, the refute_these block may serve as both a safety net and a prototype of a future unit test.

The same may be written without polluting the calling package's namespace:

    use Assert::Refute;

    refute_these {
        my $report = shift;
        $report->is( $foo, 42, "Meaning of life" );
        $report->like( $bar, qr/f?o?r?m?a?t?/, "Text as expected" );
    };

For well-written and well-tested code the use cases may be more subtle. Still some invariants may be worth a runtime check just in case.

The consequences of both passing and failing assertion block can be fine-tuned, as in:

    use Assert::Refute {
        on_fail => 'croak',
        on_pass => sub { my $report = shift; $logger->debug(...); },
    };

See "EXPORT" and "configure" below. See also Assert::Refute::Exec for the underlying object-oriented interface.

EXPORT

Per-package configuration parameters can be passed as hash refs in use statement. Anything that is not hash is passed to Exporter module:

    use Assert::Refute { on_fail => 'croak' }, "refute_these";

Or more generally (this actually dies because foo and bar parameters are not expected):

    use Assert::Refute { foo => 42 }, "refute", "contract", { bar => 137 };

Valid configuration parameters are (see "configure" below):

  • on_pass => skip|carp|croak - what to do when conditions are met. The default is skip, i.e. do nothing.

  • on_fail => skip|carp|croak - what to do when conditions are not met. The default is carp (issue a warning and continue on, even with wrong data).

  • driver => class - specify an Assert::Refute::Exec subclass to actually execute the tests, if you need to.

All of the below functions are exported by default.

    use Assert::Refute;

as well as

    use Assert::Refute qw(:core);

would only export contract, refute, contract_is, subcontract, and current_contract functions.

Also for convenience some basic assumptions mirroring the Test::More suite are exportable via :all export tag.

    use Assert::Refute qw(:all);

would export the following testing primitives:

is, isnt, ok, use_ok, require_ok, cmp_ok, like, unlike, can_ok, isa_ok, new_ok, contract_is, subcontract, is_deeply, note, diag.

See Assert::Refute::T::Basic for more.

This distribution also bundles some extra conditions:

Those need to be used explicitly.

refute_these { ... }

Refute several conditions, warn or die if they fail, as requested during use of this module. The coderef shall accept one argument, the contract execution object (likely a Assert::Refute::Exec, see need_object above).

More arguments MAY be added in the future. Return value is ignored. A contract report object is returned instead.

This is basically what one expects from a module in Assert::* namespace.

[EXPERIMENTAL] This name is preliminary and is likely to change in the nearest future. It will stay available (with a warning) for at least 5 releases after that.

refute( $condition, $message )

Test one condition in scope of the current contract.

The test passes if the $condition is false, and fails otherwise. $condition is then assumed to be the reason of failure. You can think of it as ok and diag combined.

Returns true for a passing test and false for a failing one. Dies if no contract is being executed as the time.

contract { ... }

Create a contract specification object for future use:

    use Assert::Refute qw(:all);

    my $spec = contract {
        my ($foo, $bar) = @_;
        is $foo, 42, "Life";
        like $bar, qr/b.*a.*r/, "Regex";
    };

    # later
    my $report = $spec->apply( 42, "bard" );
    $report->get_count;  # 2
    $report->is_passing; # true
    $report->get_tap;    # printable summary *as if* it was Test::More

The same may be written as

    my $spec = contract {
        my ($report, @args) = @_;
        $report->is( ... );
        $report->like( ... );
    } need_object => 1;

The need_object form may be preferable if one doesn't want to pollute the main namespace with test functions (is, ok, like etc) and instead intends to use object-oriented interface.

Other options are TBD.

Note that contract does not validate anything by itself, it just creates a read-only Assert::Refute::Contract object sitting there and waiting for an apply call.

The apply call returns a Assert::Refute::Exec object containing results of specific execution.

This is much like prepare / execute works in DBI.

See Assert::Refute::Contract for the underlying object-oriented interface.

subcontract( "Message" => $contract, @arguments )

Execute a previously defined contract and fail loudly if it fails.

[NOTE] that the message comes first, unlike in refute or other test conditions, and is required.

A contract may be an Assert::Refute::Contract object or just a subroutine accepting an Assert::Refute::Exec as first argument.

For instance, one could apply a previously defined validation to a structure member:

    my $valid_email = contract {
        my $email = shift;
        # ... define your checks here
    };

    my $valid_user = contract {
        my $user = shift;
        is ref $user, 'HASH'
            or die "Bail out - not a hash";
        like $user->{id}, qr/^\d+$/, "id is a number";
        subcontract "Check e-mail" => $valid_email, $user->{email};
    };

    # much later
    $valid_user->apply( $form_input );

Or pass a definition as argument to be applied to specific structure parts (think higher-order functions, like map or grep).

    my $array_of_foo = contract {
        my ($is_foo, $ref) = @_;

        foreach (@$ref) {
            subcontract "Element check", $is_foo, $_;
        };
    };

    $array_of_foo->apply( $valid_user, \@user_list );

current_contract

Returns the Assert::Refute::Exec object being worked on. Dies if no contract is being executed at the time.

This is actually a clone of "current_contract" in Assert::Refute::Build.

STATIC METHODS

Use these methods to configure Assert::Refute globally. There's of course always purely object-oriented Assert::Refute::Contract for even more fine-grained control.

configure

    Assert::Refute->configure( \%options );
    Assert::Refute->configure( \%options, "My::Package");

Set per-caller package configuration values for given package. configure is called implicitly by use Assert::Refute { ... } if parameter hash is present.

These are adhered to by "refute_these", mostly.

Available %options include:

  • on_pass - callback to execute if tests pass (default: skip)

  • on_fail - callback to execute if tests fail (default: carp, but not just Carp::carp - see below).

  • driver - use that class instead of Assert::Refute::Exec as execution report.

The callbacks MUST be either a CODEREF accepting Assert::Refute::Exec object, or one of predefined strings:

  • skip - do nothing;

  • carp - warn the stringified report;

  • croak - die with stringified report as error message;

Returns the resulting config (with default values added,etc).

get_config

Returns configuration from above, initializing with defaults if needed.

EXTENDING THE SUITE

Although building wrappers around refute call is easy enough, specialized tool exists for doing that.

Use Assert::Refute::Build to define new checks as both prototyped exportable functions and their counterpart methods in Assert::Refute::Exec. These functions will perform absolutely the same under control of refute_these, contract, and Test::More:

    package My::Prime;

    use Assert::Refute::Build;
    use parent qw(Exporter);

    build_refute is_prime => sub {
        my $n = shift;
        return "Not a natural number: $n" unless $n =~ /^\d+$/;
        return "$n is not prime" if $n <= 1;
        for (my $i = 2; $i*$i <= $n; $i++) {
            return "$i divides $n" unless $n % $i;
        };
        return '';
    }, args => 1, export => 1;

Much later:

    use My::Prime;

    is_prime 101, "101 is prime";
    is_prime 42, "Life is simple"; # not true

Note that the implementation sub {...} only cares about its arguments, and doesn't do anything except returning a value - it's a pure function.

Yet the exact reason for $n not being a prime will be reflected in test output.

One can also subclass Assert::Refute::Exec to create new drivers, for instance, to register failed/passed tests in a unit-testing framework of choice or generate warnings/exceptions when conditions are not met.

That's how Test::More integration is done - see Assert::Refute::Driver::More.

WHY REFUTE

Communicating a passing test normally requires 1 bit of information: everything went as planned. For failing test, however, as much information as possible is desired.

Thus refute($condition, $message) stands for an inverted assertion. If $condition is false, it is regarded as a success. If it is true, however, it is considered to be the reason for a failing test.

This is similar to how Unix programs set their exit code, or to Perl's own $@ variable, or to the falsifiability concept in science.

A subcontract is a result of multiple checks, combined into a single refutation. It will succeed silently, yet spell out details if it doesn't pass.

These primitives can serve as building blocks for arbitrarily complex assertions, tests, and validations.

BUGS

This module is still in ALPHA stage.

Test coverage is maintained at >90%, but who knows what lurks in the other 10%.

See https://github.com/dallaylaen/assert-refute-perl/issues to browse old bugs or report new ones.

SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Assert::Refute

You can also look for information at:

ACKNOWLEDGEMENTS

LICENSE AND COPYRIGHT

Copyright 2017 Konstantin S. Uvarin. <khedin at gmail.com>

This program is free software; you can redistribute it and/or modify it under the terms of the the Artistic License (2.0). You may obtain a copy of the full license at:

http://www.perlfoundation.org/artistic_license_2_0

Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License. By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license.

If your Modified Version has been derived from a Modified Version made by someone other than you, you are nevertheless required to ensure that your Modified Version complies with the requirements of this license.

This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder.

This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement, then this Artistic License to you shall terminate on the date that such litigation is filed.

Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.