The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Test::Conditions - test multiple conditions across a large data structure or list in a simple and compact way

VERSION

Version 0.8

SYNOPSIS

    $tc = Test::Conditions->new;
    
    foreach my $node ( @list )
    {
        $tc->flag('foo missing', $node->{name})
            unless defined $node->{foo};
        $tc->flag('bar missing', $node->{name})
            unless defined $node->{bar} && $node->{bar} > 0;
    }
    
    $tc->ok_all("all nodes have proper attributes");

DESCRIPTION

The purpose of this module is to facilitate testing complex data structures such as trees, lists of hashes, results of database queries, etc. You may want to run certain tests on each node or row, and report the results in a compact way. You might, for example, wish to test a list or other structure with 1,000 nodes and report the result as a single test rather than multiple thousands of individual tests. This module provides a far more flexible approach than the is_deeply method of Test::More.

An object of class Test::Conditions can keep track of any number of conditions, and reports a single event when its ok_all method is called. Under the most common usage, the test fails if one or more conditions are flagged, and succeeds if none are. Each condition which has been flagged is reported as a separate diagnostic message. Futhermore, if the nodes or other pieces of the data structure have unique identifiers, you can easily arrange for Test::Conditions to report the identifier of one of the failing nodes to help you in diagnosing the problem.

Conditions

Each separate condition that you wish to test is indicated by a key. This can be any non-empty string that is not a number. You can "set" or "clear" any condition, and you can specify whether or not this condition is expected to be set. After many set and/or clear operations, you can execute a single test using "ok_all" that will pass and fail depending on whether any conditions are set.

Labels

Instead of just setting a condition, you can "flag" it. This involves specifying some string (a label) to indicate where in the data that you are testing this condition occurs. This could represent a database key, or a node name or address, or anything else that will indicate useful information about where the condition occurred. A condition can be flagged multiple times, and will be reported only once. The first non-empty label that was flagged will be reported as well.

Positive and negative conditions

A condition can be a positive or a negative one, depending on whether it is expected or not. If you specify that a particular condition is expected, then "ok_all" will pass if that condition has been set and fail if not. If a condition is not expected, then the situation is reversed.

METHODS

new

This class method creates a new Test::Conditions instance. This instance can then be used to record whether some set of conditions has been set or cleared, and to execute a single test encapsulating this result.

Setting and clearing of conditions

set ( key )

Sets the specified condition. The single argument must be a scalar whose value is the name (key) of the condition to be set.

clear ( key )

Clears the specified condition. The single argument must be a scalar whose value is the name (key) of the condition to be cleared.

flag ( key, [ label ] )

Sets the specified condition, and can also record an arbitrary label. This label can be any non-empty string, but it is best to use some key value or node field that will indicate where in the set of data being tested the condition occurred. The first non-empty label to be flagged for any particular condition will be reported when a test fails due to that condition, so that you can use that information for debugging purposes. The number of times each condition is flagged is also recorded, and minimum and maximum limits can also be specified. See "limit_max" and "expect_min" below.

In general, you will want to use either 'set' or 'flag' with any particular condition, and not both. It is generally best to use 'set' for conditions that reflect a problem with the data structure as a whole, and 'flag' for conditions that are specific to a particular piece of it.

decrement ( condition, [ label ] )

This method decrements the count of how many times the specified condition has been flagged. If a label is specified, and if that label matches the label stored for this condition, it is cleared. Basically, if this method is called immediately after "flag" and with the same arguments, the effect of the flag will be undone. This method only exists so that if 'flag' has been called in error the effect can be reversed.

If a call to this method results in the count reaching zero, the condition is cleared.

expect ( condition... )

This method marks one or more conditions as expected. Subsequently, "ok_all" will fail unless all of the expected conditions are set. This is how you specify positive conditions instead of negative ones. For example:

    $tc = Test::Conditions->new;
    
    $tc->expect('found aaa', 'found bbb');
    
    foreach my $node ( @list )
    {
        $tc->flag('found aaa', $node->{name}) if $node->{key} eq 'aaa';
        $tc->flag('found bbb', $node->{name}) if $node->{key} eq 'bbb';
    }
    
    $tc->ok_all("found both keys");
    
    if ( $tc->is_set('found aaa') )
    {
        my $node_name = $tc->get_label('found aaa');
        diag("    Found key 'aaa' at node '$node_name'");
    }

You can use both positive (expected) and negative (non-expected) conditions together. A call to "ok_all" will succeed precisely when all of the expected conditions have been set and no non-expected conditions have.

expect_min ( condition, n )

This method indicates that the specified condition is expected to be flagged at least n times. If it is flagged fewer times than that, or not at all, then "ok_all" will fail. Calling this method with a count of 1 is exactly the same as calling "expect" on the same condition.

limit_max ( condition, n )

This method indicates that the specified condition should be flagged at most n times. If it is flagged more times than that, then "ok_all" will fail. You can use this, for example, if you expect a few nodes in your data structure to be missing particular fields but you want the test to fail if more than a certain number are.

Testing

ok_all ( test_name )

This method will execute a single test, with the specified string as the test name. The test will pass if all expected (positive) conditions are set, and if no non-expected (negative) conditions are set.

If a negative condition was flagged rather than set, then a diagnostic message will be printed indicating the label with which it was first flagged, and the total number of times it was flagged. If you set these labels based on keys or node names or other indications of where in the data structure is being tested, this can help you to figure out what is going wrong.

If a minimum and/or maximum limit has been set on a particular condition, then the test will pass only if the number of times the condition was flagged does not fall outside of these limits.

All conditions that are tested by this method are marked as being tested. Subsequent calls to 'ok_all' or 'ok_condition' will ignore them, unless they have been explicitly set or cleared afterward. However, methods such as 'is_set', 'get_count', etc. will still work on it.

ok_condition ( condition, test_name )

This method will test a single condition, and will pass or fail the specified test name. If the condition is expected, then it will pass only if set. If it is not expected, then it will pass only if not set.

If a minimum and/or maximum limit has been set on this condition, then the test will pass only if the number of times the condition was flagged does not fall outside of these limits.

The condition that is tested by this method is marked as being tested. Subsequent calls to 'ok_all' or 'ok_condition' will ignore it, unless it has0 been explicitly set or cleared afterward. However, methods such as 'is_set', 'get_count', etc. will still work on it.

Accessors

The following methods can be used to check the status of any condition

is_set ( condition )

Returns 1 if the condition is set, 0 if it has been explicitly cleared, and undef if it has been neither set nor cleared.

is_tested ( condition )

Returns 1 if "ok_all" or "ok_condition" has been called on this condition, and it has not been set or cleared since.

get_count ( condition )

Returns the number of times the condition has been flagged, or undef if it has never been flagged.

get_label ( condition )

Returns the label stored for this condition, or undef if it has never been flagged with a non-empty label.

active_conditions ( )

Returns a list of all conditions that are currently set but have not yet been tested.

expected_conditions ( )

Returns a list of all conditions that are currently expected.

all_conditions ( )

Returns a list of all conditions that have been set or cleared, regardless of whether or not they have been tested.

Resetting

If you have set up expected conditions and/or limits, you may wish to run the same Test::Conditions instance on more than one data structure. Once you have run "ok_all" on a given instance, all of the active conditions are marked as "tested" and will be ignored from then on unless subsequently set or cleared. So you can go ahead and use the same instance to test multiple bodies of data and the results will be correct. It is okay to call 'ok_all' or 'ok_condition' as many times as needed. At each call, only the status of those conditions that have been explicitly set or cleared since the last call will be considered.

If you wish to reset some or all conditions without calling 'ok_all' or 'ok_condition', you can use the following methods:

reset_conditions ( )

This method resets the status of all conditions, as if they had never been set or cleared. Limits and expects are preserved.

reset_condition ( condition )

This method resets the status of a single condition.

AUTHOR

Michael McClennen

BUGS

Please report any bugs or feature requests to bug-test-conditions at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Test-Conditions. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

LICENSE AND COPYRIGHT

Copyright 2018 Michael McClennen.

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.