On Friday 20 June 2003 20:21, Ken Williams wrote:
> Second, I find it very confusing that all these different capabilities 
> are happening inside one cmp_deeply() function.  In Perl it's much more 
> common to use the function/operator to indicate how comparisons will be 
> done - for example, <=> vs. cmp, or == vs. eq.  I would much rather see 
> these things broken up into their own functions.

I had a hard time trying to document this module and I wasn't sure I did a 
good job, now I'm certain I didn't! I hope I can explain in this email. It's 
a bit long but I hope you will see at the end that your comments are based on 
a misunderstanding of what Test::Deep does. I'd really appreciate it if you 
could tell me if it makes any sense to you or if it makes no sense at all I 
don't want to alienate users just because my docs are unintelligible. As a 
bonus, since you're @mathforum.org I'll throw in some non-well-founded set 
theory near the end ;-)

First off, the Test::Deep functions set(), bool(), bag(), re() etc are not 
comparison functions, they are shortcuts to Test::Deep::Set->new, 
Test::Deep::Bool->new, Test::Deep::Bag->new, Test::Deep::Regex->new. The 
objects they return act as markers to tell Test::Deep that at this point of 
the comparison to stop doing a simple comparison and to hand over control to 
Test::Deep::Whatever.

There's nothing you can do in regular expression that you can't do with substr 
and eq but regular expressions allow you to express complex tests in a simple 
form. That is the goal of Test::Deep. Perl has regexs that operate on a 
linear array of characters, Test::Deep supplies "regular expressions" that 
operate on an arbitrarily complicated graph of data and just as a regex often 
looks like the strings it will match, a Test::Deep structure should look like 
the structure it will match.

What's wrong with using Test::More::is_deeply()? Well, is_deeply is  just the 
complex-structure equivalent of eq for strings. is_deeply checks that two 
data structures are identical. What do you do if part of part of the 
structure you're testing is unpredictable? Maybe it comes from an outside 
source that your test script can't control, maybe it's an array in an 
undetermined order or maybe it contains an object from another module - you 
don't want your test to look inside other modules' objects because you have 
no way of telling if it's right or wrong. In these cases is_deeply() will 
fail and so is no use. Test::Deep::cmp_deeply() has varying definition of 
"equality" and so can perform tests that is_deeply can't.

Time for some examples.

Simple string case: Say you want to test a string that is returned from the 
function fn(). You know it should be "big john". So you do

Test::More::is(fn(), "big john", "string ok");

Messy string case: Things change, now fn() returns a string that contains "big 
john" and some other stuff, you can't be sure what the other stuff is, all 
you know is that the string should be a number, followed by "big john", 
possibly followed by some other stuff. No problem

Test::More::like(fn(), qr/^\d+big john.*/, "string ok");

Now imagine that you have a function that returns a hash

Simple structure case: you want to test that fn() returns

{
        age => 34,
        id => "big john",
        cars => ['toyota', 'fiat', 'citroen'],
        details => [...] # some horrible complicated object
}

Test::More::is_deeply(fn(), 
        {
                age => 34,
                id => "big john",
                cars => ['toyota', 'fiat', 'citroen'],
                details => [...] # some horrible complicated object
        }
);

Messy structure case: same as above but say now the id is no longer simply 
"big john", it's the same messy thing we talked about in the messy string 
case, and say you're no longer guaranteed that the cars will come back in any 
particular order because they're coming from an unorderd SQL query.

Test::is_deeply is no good now as it needs exact equality. You could write

my $hash = fn();
is($hash->{age}, 34);
like($hash->{id}, qr/^\d+big john.*/);
is_deeply(sort @{$hash->{cars}}, ['citroen', 'fiat', 'toyota' ]);
is_deeply($hash->{details}, [...]);
is(scalar keys %$hash, 4);

but you'd be soooooooo wrong because you've also got to check that all your 
refs are defined before you go derefing them so here's the full ugliness you 
really need

if( is(Sclar::Util::reftype($hash),  "HASH") )
{
        is($hash->{age}, 34);
        like($hash->{id}, qr/^\d+big john.*/);

        if( is(Sclar::Util::reftype($hash->{cars}),  "ARRAY") )
        {
                is_deeply(sort @{$hash->{cars}}, ['citroen', 'fiat', 'toyota' ]);
        }
        else
        {
                fail("no array");
        }
        if( is(Sclar::Util::reftype($hash->{details}),  "ARRAY") )
        {
                is_deeply($hash->{details}, [...]);
        }
        else
        {
                fail("no array");
        }
}
else
{
        for (1..6) # cos we don't want to mess up the plan!
        {
                fail("no hash");
        }
}

Notice the complete lack of test names as by now you've lost the will to test 
properly.

Test::Deep to the rescue!!

Test::Deep::cmp_deeply($hash, 
        {
                age => 34,
                id => re(qr/^\d+big john.*/),
                cars => bag('toyota', 'fiat', 'citroen'),
                details => [...] # some horrible complicated object
        },
        "check big john"
);

You'll get informative error messages if $hash is wrong and your test will 
never explode because of undefined values.

Here's a walk through how the comparison works, rather than refererring to the 
structure above as "the second argument to cmp_deeply", I'll call if $exp 
(short for expected). So

$exp = {
        age => 34,
        id => re(qr/^\d+big john.*/), # Test::Deep::Regex constructor
        cars => bag('toyota', 'fiat', 'citroen'), # Test::Deep::Set constructor
        details => [...] # some horrible complicated object
};
cmp_deeply($hash, $exp, "check big john");

works like this

- call descend($hash, $exp)
  - $exp is a HASH ref and so is $hash so call descend_hash($exp,$hash)
    - get a key from $exp - "age"
    - call descend($hash->{"age"}, $exp->{"age"})
        - $hash->{"age"} and $exp->{"age"} are both scalars
        - check $hash->{"age"} eq $exp->{"age"}
    - get next key from $exp - "id"
    - call descend($hash->{"id"}, $exp->{"id"})

      - $exp->{id} is a Test::Deep::Regex object, this means we don't just do 
a simple eq, instead we hand control over to the Test::Deep::Regex module by 
calling $exp->{id}->descend and passing in $hash->{id}

        - what happens inside Test::Deep::Regex::descend is a mystery that 
Test::Deep knows nothing about, all it wants is a return code of 1 or 0. It 
get a 1, and breathes a sigh of relief

    - get next key from $exp - "cars"
    - call descend($hash->{"cars"}, $exp->{"cars"})

      - $exp->{id} is a Test::Deep::Set object so again call it's descend 
method

    - the Set comparison was OK
    - get next key from $exp - "details"
    - call descend($hash->{"details"}, $exp->{"details"})
      - $hash->{"details"} and $exp->{"details"} are both ARRAY refs so call
descend_array
        - much descending and comparing

    - there is no next key in $exp, did we use up all the keys in $hash? Yes

All stages of the test passed, so it's a pass. If it had failed, then you get 
dagnostics like

# checking $data->{"age"}
# expected: "34"
# got: "27"

or 

# checking $data->{"id"} as a regex
# expected: something matching qr/\d+big john.*/
# got: "14556big mick"


Other types of comparison are

bool - make sure something is true or false, without caring exactly what it is
ignore - don't look any further into this part of the structure
isa - check that it's a blessed ref from this class
methods - call these various methods and make sure they return these values
shallow - check that it is exactly this reference rather than delving into it

and several more, with more to come. Plus you can define your own and even 
export them for other modules to use.

So I hope you can see now why there is only 1 comparison function and why all 
the other functions can't be "broken up".

Now for the set theory. Imagine 3 sets A, B and C and 3 items x, y, z. Here's 
what our sets look like

A = (B, C, x)
B = (A, C, y)
C = (A, B, z)

In traditional set theory this would be illegal. There is the axiom of 
foundation which roughly says that a set S cannot be an element of itself or 
of any of it's elements or of any of their elements or ... Basically S can't 
appear inside S, now matter how deeply buried. Some really smart guy proved 
that this axiom is independent of the other axioms of set theory. This means 
you can assume it's true and you get one set of theorems or you can assume 
it's false and you get another different set of equally valid theorems. This 
is good thing, otherwise

$a = [$a];

would be probably make your computer explode.

What's this to do with Test::Deep? Due to Test::Deep working the way it does 
and being able to handle circular refs you get and almost useless but, I 
think, quite cool feature. Test::Deep is able to test nasty non-wellfounded 
set comparions. So for the example above:

my $A = set();
my $B = set();
my $C = set();

$A->add("x", $B);
$B->add("y", $C);
$C->add("z", $A);
$A->add($C);
$B->add($A);
$C->add($B);
# that took a bit longer than it should have due to an "issue" I just found

my $D = [];
my $E = [];
my $F = [];

push(@$D, "x", $E, $F, "x", $E, $F);
push(@$E, $D, $D, "y", $F);
push(@$F, $E, $D, $D, "z");

cmp_deeply($D, $A); # because dups and order don't matter this is a yes

push($F, $F);

# now we have
# F = (D, E, F, "z") instead of (D, E, "z")

cmp_deeply($D, $A); # no although it takes a little while to decide

Please let me know whether this made any sense. If you have any suggestions 
how I can explain Test::Deep in 20 words, I'd really appreciate it!

F

Reply via email to