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