Skip to content

Commit

Permalink
lexically scoped strictness
Browse files Browse the repository at this point in the history
  • Loading branch information
DrHyde authored and geofffranks committed Jan 5, 2021
1 parent e57fc5a commit 3ba8bc7
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 13 deletions.
3 changes: 3 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
Revision history for Test::MockModule

vX.XXX.X
- XXXXXXX 'strict' mode is now lexically scoped

v0.175.0
- 964aa2a Ignore CI files and whitesource - Nicolas R

Expand Down
90 changes: 81 additions & 9 deletions lib/Test/MockModule.pm
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,35 @@ use Carp;
use SUPER;
$VERSION = '0.175.0';

our $STRICT_MODE;

sub import {
my ( $class, @args ) = @_;

# default if no args
$^H{'Test::MockModule/STRICT_MODE'} = 0;

foreach my $arg (@args) {
if ( $arg eq 'strict' ) {
$STRICT_MODE = 1;
}
else {
$^H{'Test::MockModule/STRICT_MODE'} = 1;
} elsif ( $arg eq 'nostrict' ) {
$^H{'Test::MockModule/STRICT_MODE'} = 0;
} else {
warn "Test::MockModule unknown import option '$arg'";
}
}

return;
}

sub _strict_mode {
my $depth = 0;
while(my @fields = caller($depth++)) {
my $hints = $fields[10];
if($hints && grep { /^Test::MockModule\// } keys %{$hints}) {
return $hints->{'Test::MockModule/STRICT_MODE'};
}
}
return 0;
}

my %mocked;
sub new {
my $class = shift;
Expand Down Expand Up @@ -102,7 +115,7 @@ sub define {
sub mock {
my ($self, @mocks) = (shift, @_);

croak "mock is not allowed in strict mode. Please use define or redefine" if $STRICT_MODE;
croak "mock is not allowed in strict mode. Please use define or redefine" if($self->_strict_mode());

return $self->_mock(@mocks);
}
Expand Down Expand Up @@ -140,7 +153,7 @@ sub _mock {
sub noop {
my $self = shift;

croak "noop is not allowed in strict mode. Please use define or redefine" if $STRICT_MODE;
croak "noop is not allowed in strict mode. Please use define or redefine" if($self->_strict_mode());

$self->_mock($_,1) for @_;

Expand Down Expand Up @@ -287,7 +300,7 @@ Test::MockModule - Override subroutines in a module for unit testing
}
# If you want to prevent noop and mock from working, you can
# load Test::MockModule in strict mode
# load Test::MockModule in strict mode.
use Test::MockModule qw/strict/;
my $module = Test::MockModule->new('Module::Name');
Expand All @@ -298,6 +311,16 @@ Test::MockModule - Override subroutines in a module for unit testing
# Dies since you specified you wanted strict mode.
$module->mock('subroutine', sub { ... });
# Turn strictness off in this lexical scope
{
use Test::MockModule 'nostrict';
# ->mock() works now
$module->mock('subroutine', sub { ... });
}
# Back in the strict scope, so mock() dies here
$module->mock('subroutine', sub { ... });
=head1 DESCRIPTION
C<Test::MockModule> lets you temporarily redefine subroutines in other packages
Expand All @@ -308,6 +331,53 @@ module. The object remembers the original subroutine so it can be easily
restored. This happens automatically when all MockModule objects for the given
module go out of scope, or when you C<unmock()> the subroutine.
=head1 STRICT MODE
One of the weaknesses of testing using mocks is that the implementation of the
interface that you are mocking might change, while your mocks get left alone.
You are not now mocking what you thought you were, and your mocks might now be
hiding bugs that will only be spotted in production. To help prevent this you
can load Test::MockModule in 'strict' mode:
use Test::MockModule qw(strict);
This will disable use of the C<mock()> method, making it a fatal runtime error.
You should instead define mocks using C<redefine()>, which will only mock
things that already exist and die if you try to redefine something that doesn't
exist.
Strictness is lexically scoped, so you can do this in one file:
use Test::MockModule qw(strict);
...->redefine(...);
and this in another:
use Test::MockModule; # the default is nostrict
...->mock(...);
You can even mix n match at different places in a single file thus:
use Test::MockModule qw(strict);
# here mock() dies
{
use Test::MockModule qw(nostrict);
# here mock() works
}
# here mock() goes back to dieing
use Test::MockModule qw(nostrict);
# and from here on mock() works again
NB that strictness must be defined at compile-time, and set using C<use>. If
you think you're going to try and be clever by calling Test::MockModule's
C<import()> method at runtime then what happens in undefined, with results
differing from one version of perl to another. What larks!
=head1 METHODS
=over 4
Expand Down Expand Up @@ -538,6 +608,8 @@ Current Maintainer: Geoff Franks <gfranks@cpan.org>
Original Author: Simon Flack E<lt>simonflk _AT_ cpan.orgE<gt>
Lexical scoping of strictness: David Cantrell E<lt>david@cantrell.org.ukE<gt>
=head1 COPYRIGHT
Copyright 2004 Simon Flack E<lt>simonflk _AT_ cpan.orgE<gt>.
Expand Down
8 changes: 8 additions & 0 deletions t/lib/ScopedStrict/Mockee1.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ScopedStrict::Mockee1;

use strict;
use warnings;

sub gonna_mock_this { return "you're not going to see this" }

1;
8 changes: 8 additions & 0 deletions t/lib/ScopedStrict/Mockee2.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ScopedStrict::Mockee2;

use strict;
use warnings;

sub also_gonna_mock_this { return "you're not going to see this either" }

1;
12 changes: 12 additions & 0 deletions t/lib/ScopedStrict/NonStrictMocker.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ScopedStrict::NonStrictMocker;

use strict;
use warnings;

use Test::MockModule;

Test::MockModule->new('ScopedStrict::Mockee2')->mock(
also_gonna_mock_this => sub { "another mocked sub" }
);

1;
12 changes: 12 additions & 0 deletions t/lib/ScopedStrict/StrictMocker.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ScopedStrict::StrictMocker;

use strict;
use warnings;

use Test::MockModule qw(strict);

Test::MockModule->new('ScopedStrict::Mockee1')->redefine(
gonna_mock_this => sub { "mocked sub" }
);

1;
26 changes: 22 additions & 4 deletions t/mock_strict.t
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use Test::MockModule qw/strict/;

my $mocker = Test::MockModule->new('Mockee');

is( $Test::MockModule::STRICT_MODE, 1, "use Test::MockModule qw/strict/; sets \$STRICT_MODE to 1" );
is( Test::MockModule->_strict_mode(), 1, "use Test::MockModule qw/strict/; sets strict mode" );

eval { $mocker->mock( 'foo', 2 ) };
like( "$@", qr/^mock is not allowed in strict mode. Please use define or redefine at/, "mock croaks in strict mode." );
Expand All @@ -22,9 +22,27 @@ is( Mockee->foo, "abc", "define is allowed in strict mode." );
$mocker->redefine( 'existing_subroutine', "def" );
is( Mockee->existing_subroutine, "def", "redefine is allowed in strict mode." );

$Test::MockModule::STRICT_MODE = 0;
$mocker->mock( 'foo', 123 );
is( Mockee->foo, 123, "mock is allowed when strict mode is turned off." );
{
use Test::MockModule 'nostrict'; # no strictness in this lexical scope
is( Test::MockModule->_strict_mode(), 0, "nostrict turns strictness off");
$mocker->mock( 'foo', 123 );
is( Mockee->foo, 123, "mock is allowed when strict mode is turned off." );
{
use Test::MockModule 'strict'; # but we are strict here again
eval { $mocker->mock( 'foo', 2 ) };
like( "$@", qr/^mock is not allowed in strict mode/,
"we can nest alternating strict/nostrict soooo deeply");
}
$mocker->mock('foo', 456);
pass("Back in a non-strict scope, the intervening strict scope didn't make ->mock() crash");
}

eval { $mocker->mock( 'foo', 2 ) };
like( "$@", qr/^mock is not allowed in strict mode. Please use define or redefine at/, "Finally, back in the original scope, and we return to being strict");

use Test::MockModule 'nostrict'; # same lexical scope as we opened in, but change how strict it is
$mocker->mock('foo', 94);
pass("Changed to nostrict in a previously strict scope, mock() didn't crash");

done_testing();

Expand Down
20 changes: 20 additions & 0 deletions t/strict_scoped_files.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use strict;
use warnings;

use Test::More;

use lib 't/lib';

# things we're going to mock
use ScopedStrict::Mockee1;
use ScopedStrict::Mockee2;

# mock one of them in strict mode
use ScopedStrict::StrictMocker;
# this doesn't turn on strict mode, and tries to use ->mock(). It
# shouldn't crash
use ScopedStrict::NonStrictMocker;

# yay, we didn't crash!
pass "Using 'strict' mode in one module that we use didn't prevent ->mock()ing in another";
done_testing();

0 comments on commit 3ba8bc7

Please sign in to comment.