File Coverage

blib/lib/Test/Mock/One.pm
Criterion Covered Total %
statement 68 68 100.0
branch 40 40 100.0
condition 8 9 88.8
subroutine 12 13 92.3
pod 3 3 100.0
total 131 133 98.5


line stmt bran cond sub pod time code
1             package Test::Mock::One;
2 1     1   88764 use warnings;
  1         9  
  1         30  
3 1     1   7 use strict;
  1         2  
  1         73  
4              
5             # ABSTRACT: Mock the world with one object
6              
7             our $VERSION = '0.007';
8              
9             our $AUTOLOAD;
10              
11 1     1   8 use overload '""' => '__x_mock_str';
  1         2  
  1         7  
12              
13 1     1   114 use List::Util 1.33 qw(any);
  1         35  
  1         86  
14 1     1   9 use Scalar::Util qw(blessed);
  1         2  
  1         688  
15              
16             sub new {
17 33     33 1 5507 my $class = shift;
18 33   66     226 return bless({@_}, ref($class) || $class);
19             }
20              
21             sub can {
22 4     4 1 749 my ($self, $can) = @_;
23 4 100 100     17 if (!exists $self->{$can} && $self->{"X-Mock-Strict"}) {
24 1         5 return 0;
25             }
26 3         6 return 1;
27             }
28              
29             sub AUTOLOAD {
30 48     48   2346 my $self = shift;
31              
32 48         255 my ($call) = $AUTOLOAD =~ /([^:]+)$/;
33              
34 48 100       118 if (exists $self->{$call}) {
    100          
35 41         76 my $ref = ref $self->{$call};
36 41 100       118 if ($ref eq 'HASH') {
    100          
    100          
    100          
37 15         25 return $self->new( __x_mock_copy_x($self), %{ $self->{$call} });
  15         41  
38             }
39             elsif ($ref eq 'ARRAY') {
40 4         12 return $self->new(__x_mock_copy_x($self), map { $_ => $self } @{ $self->{$call} });
  10         23  
  4         9  
41             }
42             elsif ($ref eq 'CODE') {
43 3 100       11 if ($self->{'X-Mock-SelfArg'}) {
44 2         6 return $self->{$call}->($self, @_);
45             }
46 1         4 return $self->{$call}->(@_);
47             }
48             elsif ($ref eq 'REF') {
49 2         4 return ${ $self->{$call} };
  2         15  
50             }
51 17         59 return $self->{$call};
52             }
53             elsif ($self->{"X-Mock-Strict"}) {
54 2         28 die sprintf("Using %s in strict mode, called undefined function '%s'",
55             __PACKAGE__, $call);
56             }
57 5         24 return $self;
58             }
59              
60             sub isa {
61 18     18 1 1836 my ($self, $class) = @_;
62              
63 18 100       74 if (my $isas = $self->{"X-Mock-ISA"}) {
    100          
64 15         23 my $ref = ref $isas;
65 15 100 100     70 if (!$ref && $isas eq $class) {
    100          
    100          
    100          
66 3         8 return 1;
67             }
68             elsif ($ref eq 'ARRAY') {
69 3 100   5   14 return 1 if any { $_ eq $class } @$isas;
  5         15  
70             }
71             elsif ($ref eq 'CODE') {
72 4 100       11 if ($self->{'X-Mock-SelfArg'}) {
73 2         6 return $isas->($self, $class);
74             }
75 2         5 return $isas->($class);
76             }
77             elsif ($ref eq "Regexp") {
78 2         15 return $class =~ /$isas/;
79             }
80 4         18 return 0;
81             }
82             elsif (exists $self->{"X-Mock-ISA"}) {
83 1         10 return 0;
84             }
85 2         4 return 1;
86             }
87              
88             # Just an empty method to prevent weird AUTOLOAD loops
89       0     sub DESTROY { }
90              
91             my @__xattr = qw(Strict ISA Stringify SelfArg);
92              
93             sub __x_mock_copy_x {
94 19     19   26 my ($orig) = @_;
95 19         23 my %copy;
96 19         36 foreach (@__xattr) {
97 76         100 my $attr = "X-Mock-$_";
98 76 100       121 if (exists $orig->{$attr}) {
99 56         86 $copy{$attr} = $orig->{$attr};
100             }
101             }
102 19         50 return %copy;
103             }
104              
105             sub __x_mock_str {
106 7     7   268 my ($self) = @_;
107 7 100       18 if (my $stringify = $self->{'X-Mock-Stringify'}) {
108 5 100       16 if (ref $stringify eq 'CODE') {
109 3 100       7 if ($self->{'X-Mock-SelfArg'}) {
110 2         6 return $stringify->($self);
111             }
112 1         3 return $stringify->();
113             }
114 2         10 return $stringify;
115             }
116 2         11 return __PACKAGE__ . " stringified";
117             }
118              
119             1;
120              
121             __END__
122              
123             =pod
124              
125             =encoding UTF-8
126              
127             =head1 NAME
128              
129             Test::Mock::One - Mock the world with one object
130              
131             =head1 VERSION
132              
133             version 0.007
134              
135             =head1 SYNOPSIS
136              
137             use Test::Mock::One;
138              
139             my $mock = Test::Mock::One->new(
140             foo => 'return value',
141             bar => 1,
142              
143             hashref => \{ foo => 'bar' },
144             arrayref => \[ foo => 'bar' ],
145             code => sub { return your_special_function() },
146              
147             );
148              
149             $mock->foo; # 'return value'
150             $mock->bar; # 1
151             $mock->hashref; # { foo => bar}
152             $mock->arrayref; # [ foo, bar ]
153             $mock->code; # executes your_special_function
154              
155             $mock->no->yes->work->it; # works fine
156              
157             In combination with L<Sub::Override>:
158              
159             my $override = Sub::Override->new('Foo::Bar::baz', sub { Test::Mock::One(foo => 'bar') });
160              
161             =head1 DESCRIPTION
162              
163             Be able to mock many things with little code by using AUTOLOAD.
164              
165             The problem this module tries to solve is to allow testing many things
166             without having to write many lines of code. If you want to create mock objects
167             you often need to write code like this:
168              
169             {
170             no warnings qw(redefine once);
171             local *Foo::thing = sub {
172             return bless({}, 'Baz');
173             };
174             local *Baz::foo = sub { return 1 };
175             local *Baz::bar = sub { return 1 };
176             local *Baz::baz = sub { return 1 };
177             use warnings;
178              
179             # Actual test here
180             }
181              
182             Test::Mock::One allows you to write a simple object that allows you to do the same with
183              
184             my $mock = Test::Mock::One->new(foo => 1, bar => 1, baz => 1);
185             # Sub::Override helps too
186             my $override = Sub::Override->new('Foo::thing' => sub { return $mock });
187              
188             # Actual test here
189              
190             You don't actually need to define anything, by default method on a
191             Test::Mock::One object will return itself. You can tweak the behaviour
192             by how you instantiate the object. There are several attributes that
193             control the object, these are defined as X-Mock attributes: X-Mock-ISA
194             to override the isa(), X-Mock-Strict to override the can() and allowed
195             methods and X-Mock-Stringify to tell it how to stringify the object.
196              
197             =head2 Example usage
198              
199             Let's say you want to test a function that retrieves a user from a
200             database and checks if it is active
201              
202             Package Foo;
203             use Moose;
204              
205             has schema => (is => 'ro');
206              
207             sub check_user_in_db {
208             my ($self, $username) = @_;
209             my $user = $self->schema->resultset('User')->search_rs(
210             { username => $username }
211             )->first;
212              
213             return $user if $user && $user->is_active;
214             die "Unable to find user";
215             }
216              
217             # In your test
218             my $foo = Foo->new(
219             schema => Test::Mock::One->new(
220             schema => {
221             resultset =>
222             { search_rs => { first => { is_active => undef } } }
223             },
224             'X-Mock-Strict' => 1,
225             )
226             );
227              
228             # Is the same as above, without Strict mode
229             $foo = Foo->new(
230             schema => Test::Mock::One->new(
231             is_active => undef
232             # This doesn't work with X-Mock-Strict enabled, because
233             # the chain schema->resultset->search_rs->first cannot be
234             # resolved
235             )
236             );
237              
238             throws_ok(
239             sub {
240             $foo->check_user_in_db('username');
241             },
242             qr/Unable to find user/,
243             "username isn't active"
244             );
245              
246             # A sunny day scenario would have been:
247             my $mock = Foo->new(schema => Test::Mock::One->new());
248             lives_ok(sub { $mock->check_user_in_db('username') },
249             "We found the user");
250              
251             =head1 METHODS
252              
253             =head2 new
254              
255             Instantiate a new Test::Mock::One object
256              
257             =over
258              
259             =item X-Mock-Strict
260              
261             Boolean value. Undefined attributes will not be mocked and calling them makes us die.
262              
263             =item X-Mock-ISA
264              
265             Mock the ISA into the given class. Supported ways to mock the ISA:
266              
267             'X-Mock-ISA' => 'Some::Pkg',
268             'X-Mock-ISA' => qr/Some::Pkg/,
269             'X-Mock-ISA' => [qw(Some::Pkg Other::Pkg)],
270             'X-Mock-ISA' => sub { return 0 },
271             'X-Mock-ISA' => undef,
272              
273             =item X-Mock-Stringify
274              
275             Tell us how to stringify the object
276              
277             'X-Mock-Stringify' => 'My custom string',
278             'X-Mock-Stringify' => sub { return "foo" },
279              
280             =item X-Mock-SelfArg
281              
282             Boolean value. Make all the code blocks use $self. This allows you to do things like
283              
284             Test::Mock::One->new(
285             'X-Mock-SelfArg' => 1,
286             code => sub {
287             my $self = shift;
288             die "We have bar" if $self->foo eq 'bar';
289             return "some value";
290             }
291             );
292              
293             This also impacts C<X-Mock-ISA> and C<X-Mock->Stringify>.
294              
295             =back
296              
297             =head2 isa
298              
299             Returns true or false, depending on how X-Mock-ISA is set.
300              
301             =head2 can
302              
303             Returns true or false, depending on how X-Mock-Strict is set.
304              
305             =head1 SEE ALSO
306              
307             L<Sub::Override>
308              
309             =head1 AUTHOR
310              
311             Wesley Schwengle <waterkip@cpan.org>
312              
313             =head1 COPYRIGHT AND LICENSE
314              
315             This software is Copyright (c) 2017 by Wesley Schwengle.
316              
317             This is free software, licensed under:
318              
319             The (three-clause) BSD License
320              
321             =cut