Beefy Boxes and Bandwidth Generously Provided by pair Networks
There's more than one way to do things
 
PerlMonks  

Mocking isa under Test::Deep

by choroba (Archbishop)
on May 01, 2016 at 20:01 UTC ( #1161984=perlmeditation: print w/replies, xml ) Need Help??

At work, we use a rather old Perl on the servers (5.10.1 on RedHat 6, but still better than 5.8.3 at my $job-1), with similarly dated distributions. We plan to upgrade, so I tried running some of the tests locally on my laptop with Perl 5.18.2 (openSUSE Leap 42.1).

Hash randomisation

Most failures were caused by the new hash randomisation (see Hash order randomization is coming, are you ready?). As we use Test::Spec, most failures can be solved easily by turning an array reference into a bag:

# Old: cmp_deeply($obj->method, [ $result1, $result2 ]); # New: cmp_deeply($obj->method, bag($result1, $result2));

Mocking objects under Moose

We use Moose in most of the code, which makes mocking a bit harder because of type constraints. Imagine you have the following code you need to test:

use warnings; use strict; { package Person; use Moose; use namespace::autoclean; has name => ( is => 'rw', isa => 'Str', required => 1, ); has id => ( is => 'ro', isa => 'Str', required => 1, ); __PACKAGE__->meta->make_immutable; } { package Position; use Moose; use namespace::autoclean; has person => ( is => 'rw', isa => 'Person', ); has title => ( is => 'ro', isa => 'Str', ); __PACKAGE__->meta->make_immutable; }

When testing the Position, we don't care about the details of the Person. We only want to stub an object with the needed methods implemented, which can even be none:

use Test::Spec; describe 'position' => sub { it 'instantiates' => sub { my $person = stub(); my $position = 'Position'->new(person => $person); isa_ok($position, 'Position'); cmp_deeply([ $position->person ], bag($person)); }; };

(The last line doesn't make much sense in this context, but imagine more complex objects. We need to use the bag function somewhere to show the problem.)

This would work under plain OO, but it doesn't for us; Moose complains:

Attribute (person) does not pass the type constraint because: Validati +on failed for 'Person' with value Test::Spec::Mocks::MockObject={ }

The common trick to solve this, appearing all over the code base, has been to stub the isa method of the object to always return 1. Moose's got happy when checking the object type, and no one else has cared:

my $person = stub( isa => 1 );

But alas, this trick doesn't work in the newer Test::Deep. Here's the failure message:

Found a special comparison in $data You can only use specials in the expects structure at /home/choroba/pe +rl5/lib/perl5/Test/Deep.pm line 346.

Pretty informative, don't you think? It took me several hours to find the exact reason; the indicated line was changed in May 2011 in the following way:

# Old: if (! $Expects and ref($d1) and UNIVERSAL::isa($d1, "Test::Deep::Cmp") +) # New: if (! $Expects and Scalar::Util::blessed($d1) and $d1->isa("Test::Deep +::Cmp"))

So, returning 1 from the isa method now makes Test::Deep believe the stubbed object is its special construct that shouldn't appear on the left hand side of the comparison.

To verify that's actually the problem, I tried to modify the isa in a more sophisticated way:

my $person = stub( isa => sub { $_[1] !~ /Test/ } );

and yes, it started to work again. Moose asks for Person , so isa returns 1, the testing framework asks for Test::Deep::Cmp and therefore gets 0.

Final solution

But it's an ugly hack. Some other modules might get confused by such a mocking, as they might check for other classes not containing Test , or, worse, we also have several classes whose namespace contains Test somewhere. So, I created a helper function

sub mock_isa { my ($class) = @_; isa => sub { $_[1] eq $class } }

which can be used as

my $person = stub(mock_isa('Person'));

Not as short as the original trick, but still easier than reimplementing the whole inheritance logic. What tricks do you use?

($q=q:Sq=~/;[c](.)(.)/;chr(-||-|5+lengthSq)`"S|oS2"`map{chr |+ord }map{substrSq`S_+|`|}3E|-|`7**2-3:)=~y+S|`+$1,++print+eval$q,q,a,

Replies are listed 'Best First'.
Re: Mocking isa under Test::Deep
by BrowserUk (Pope) on May 01, 2016 at 23:40 UTC

    Do I understand this correctly?

    The authors of a test tool have unilaterally decided that you (nor any of their thousands of other users) are allowed to use a comparator function that they didn't provide, breaking your test suite in the bargin, and you are meditating on the way you bypassed their arrogant caprice?


    With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority". I knew I was on the right track :)
    In the absence of evidence, opinion is indistinguishable from prejudice.
      The authors of a test tool have unilaterally decided that you [...] are [not] allowed to use a comparator function that they didn't provide,

      A more accurate summary would be that a test tool uses a special class as a sentinel and complains if a sentinel value turns up in a place where it has no business being. Meanwhile, a sloppy hack to get around draconian type checking had the side effect of making a non-sentinel appear to be a sentinel. The solution was to make the hack slightly less sloppy.

      I would put more blame on the type checking that requires the hack than on the use of an internal class name as a way to create and detect sentinels.

      - tye        

        A more accurate summary would be ...

        I'll take your word on that. I just looked through it all again and I can see that my original conclusion was less than the full story.

        That said, the breaking change is to Test::Deeply; appears (to me) to be digging into the guts of perl (via Scalar::Util::blessed()) to prevent their users from using fairly ordinary OO techniques to bypass their checks; and breaking stuff that worked for their users, all for the sake of the unjustified* and nebulous goal of 'improvement' of their own code base.

        That's akin to Stanley embossing a weak point on the shafts of their screwdrivers to prevent users from using them as levers.

        (*As in, not explained anywhere I could find.)


        With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority". I knew I was on the right track :)
        In the absence of evidence, opinion is indistinguishable from prejudice.

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlmeditation [id://1161984]
Approved by hippo
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others cooling their heels in the Monastery: (5)
As of 2019-12-08 17:52 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found

    Notices?