From 3ba8bc7f3f5b4d223a4a3544e1d5b03129fad11e Mon Sep 17 00:00:00 2001 From: David Cantrell Date: Thu, 5 Nov 2020 18:08:43 +0000 Subject: [PATCH] lexically scoped strictness --- Changes | 3 + lib/Test/MockModule.pm | 90 ++++++++++++++++++++++++--- t/lib/ScopedStrict/Mockee1.pm | 8 +++ t/lib/ScopedStrict/Mockee2.pm | 8 +++ t/lib/ScopedStrict/NonStrictMocker.pm | 12 ++++ t/lib/ScopedStrict/StrictMocker.pm | 12 ++++ t/mock_strict.t | 26 ++++++-- t/strict_scoped_files.t | 20 ++++++ 8 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 t/lib/ScopedStrict/Mockee1.pm create mode 100644 t/lib/ScopedStrict/Mockee2.pm create mode 100644 t/lib/ScopedStrict/NonStrictMocker.pm create mode 100644 t/lib/ScopedStrict/StrictMocker.pm create mode 100644 t/strict_scoped_files.t diff --git a/Changes b/Changes index 469a0f8..2eea107 100644 --- a/Changes +++ b/Changes @@ -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 diff --git a/lib/Test/MockModule.pm b/lib/Test/MockModule.pm index f683253..219b398 100644 --- a/lib/Test/MockModule.pm +++ b/lib/Test/MockModule.pm @@ -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; @@ -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); } @@ -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 @_; @@ -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'); @@ -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 lets you temporarily redefine subroutines in other packages @@ -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 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 method, making it a fatal runtime error. +You should instead define mocks using C, 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. If +you think you're going to try and be clever by calling Test::MockModule's +C method at runtime then what happens in undefined, with results +differing from one version of perl to another. What larks! + =head1 METHODS =over 4 @@ -538,6 +608,8 @@ Current Maintainer: Geoff Franks Original Author: Simon Flack Esimonflk _AT_ cpan.orgE +Lexical scoping of strictness: David Cantrell Edavid@cantrell.org.ukE + =head1 COPYRIGHT Copyright 2004 Simon Flack Esimonflk _AT_ cpan.orgE. diff --git a/t/lib/ScopedStrict/Mockee1.pm b/t/lib/ScopedStrict/Mockee1.pm new file mode 100644 index 0000000..6133655 --- /dev/null +++ b/t/lib/ScopedStrict/Mockee1.pm @@ -0,0 +1,8 @@ +package ScopedStrict::Mockee1; + +use strict; +use warnings; + +sub gonna_mock_this { return "you're not going to see this" } + +1; diff --git a/t/lib/ScopedStrict/Mockee2.pm b/t/lib/ScopedStrict/Mockee2.pm new file mode 100644 index 0000000..2c3b158 --- /dev/null +++ b/t/lib/ScopedStrict/Mockee2.pm @@ -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; diff --git a/t/lib/ScopedStrict/NonStrictMocker.pm b/t/lib/ScopedStrict/NonStrictMocker.pm new file mode 100644 index 0000000..93e3714 --- /dev/null +++ b/t/lib/ScopedStrict/NonStrictMocker.pm @@ -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; diff --git a/t/lib/ScopedStrict/StrictMocker.pm b/t/lib/ScopedStrict/StrictMocker.pm new file mode 100644 index 0000000..fef8a70 --- /dev/null +++ b/t/lib/ScopedStrict/StrictMocker.pm @@ -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; diff --git a/t/mock_strict.t b/t/mock_strict.t index a95c198..252a430 100644 --- a/t/mock_strict.t +++ b/t/mock_strict.t @@ -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." ); @@ -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(); diff --git a/t/strict_scoped_files.t b/t/strict_scoped_files.t new file mode 100644 index 0000000..e7f7459 --- /dev/null +++ b/t/strict_scoped_files.t @@ -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();