File Coverage

blib/lib/Test/Mock/Recorder.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Test::Mock::Recorder;
2 8     8   23680 use strict;
  8         22  
  8         382  
3 8     8   40 use warnings;
  8         14  
  8         278  
4 8     8   48 use base qw(Class::Accessor::Fast);
  8         12  
  8         7122  
5             __PACKAGE__->mk_ro_accessors(qw(_mock _expectations));
6 8     8   30089 use Test::MockObject;
  0            
  0            
7             use Test::Mock::Recorder::Expectation;
8             use Test::Builder;
9             use UNIVERSAL::isa;
10              
11             our $VERSION = '0.01';
12              
13             =head1 NAME
14              
15             Test::Mock::Recorder - Record-and-verify style mocking library.
16              
17             =head1 SYNOPSIS
18              
19             my $rec = Test::Mock::Recorder->new;
20             $rec->expects('print')->with('hello');
21            
22             $rec->verify_ok(
23             sub { my $io = shift; $io->print('hello'); }
24             );
25            
26             # If you don't like callback-style interface...
27             my $io = $rec->replay;
28             $io->print('hello');
29             $rec->verify_ok($io);
30              
31             =head1 DESCRIPTION
32              
33             Test::Mock::Recorder is a record-and-verify style mocking library.
34              
35             It wraps Test::MockObject and provides functionality of
36             testing a sequence of method calls.
37              
38             =head1 CLASS METHODS
39              
40             =head2 new()
41              
42             Constructor.
43              
44             =cut
45              
46             sub new {
47             my ($class) = @_;
48             return $class->SUPER::new({
49             _expectations => [],
50             });
51             }
52              
53             =head1 INSTANCE METHODS
54              
55             =head2 expects($method)
56              
57             Append exceptation of calling method named $method and
58             returns new Test::Mock::Recorder::Expectation instance.
59              
60             =cut
61              
62             sub _expects_one {
63             my ($self, $method) = @_;
64              
65             my $result = Test::Mock::Recorder::Expectation->new({ method => $method });
66             push @{ $self->_expectations }, $result;
67              
68             return $result;
69             }
70              
71             =head2 expects($method1 => $ret1, $method2 => $ret2, ...)
72              
73             Short-form of one-argument "expects" method.
74              
75             =cut
76              
77             sub _slice {
78             my ($n, @src) = @_;
79              
80             my $max = scalar @src / $n - 1;
81             my @result;
82             for my $i (0...$max) {
83             push @result, [ map { $src[$i * $n + $_] } 0...$n ];
84             }
85             return @result;
86             }
87              
88             sub expects {
89             my $self = shift;
90              
91             if (scalar @_ == 1) {
92             return $self->_expects_one(@_);
93             }
94              
95             for (_slice(2, @_)) {
96             my ($method, $return) = @{ $_ };
97             $self->_expects_one($method)->returns($return);
98             }
99             }
100              
101             =head2 replay(), replay($callback)
102              
103             Creates new mock object and pass it.
104              
105             Without $callback, the method returns new mock object.
106              
107             With $callback, the method pass a new mock to $callback and verify,
108             returns the result of verification.
109              
110             =cut
111              
112             sub replay {
113             my ($self, $callback) = @_;
114              
115             my $mock = $self->_replay;
116              
117             if ($callback) {
118             $callback->($mock);
119             return $self->verify($mock);
120             } else {
121             return $mock;
122             }
123             }
124              
125             sub _nth {
126             my ($n) = @_;
127              
128             [ qw(first second third) ]->[ $n-1 ] || "${n}nd";
129             }
130              
131             sub _create_mock_method {
132             my ($self, $expectation, $index_ref) = @_;
133              
134             return sub {
135             my $where =
136             sprintf('%s invocation of the mock', _nth($$index_ref + 1));
137             my $e = $self->_expectations->[ $$index_ref ];
138             $$index_ref++;
139             if (! $e) {
140             die sprintf(
141             'The %s is "%s" but not expected',
142             $where,
143             $expectation->method,
144             );
145             }
146              
147             my $ret;
148             eval {
149             $ret = $e->verify(@_);
150             };
151             if ($@) {
152             if ($@->isa('Test::Mock::Recorder::InvalidArguments')) {
153             die sprintf(
154             'Called "%s" with invalid arguments at the %s',
155             $@->method,
156             $where,
157             );
158             } else {
159             die $@;
160             }
161             } else {
162             return $ret;
163             }
164             };
165             }
166              
167             sub _replay {
168             my ($self) = @_;
169              
170             my $result = Test::MockObject->new;
171             my $called = 0;
172              
173             for my $e (@{ $self->_expectations }) {
174             $result->mock(
175             $e->method => $self->_create_mock_method($e, \$called)
176             );
177             }
178              
179             return $result;
180             }
181              
182             =head2 verify($mock)
183              
184             Verify $mock and returns true when success.
185              
186             =cut
187              
188             sub verify {
189             my ($self, $mock) = @_;
190              
191             my $i = 1;
192             for my $expectation (@{ $self->_expectations }) {
193             my $where =
194             sprintf('%s invocation of the mock', _nth($i));
195              
196             my @actual = $mock->next_call;
197             if (! $actual[0]) {
198             die sprintf(
199             q{The %s is "%s" but not called},
200             $where, $expectation->method
201             );
202             }
203             if ($actual[0] && $actual[0] eq $expectation->method) {
204             ;
205             } else {
206             die sprintf(
207             q{The %s should be "%s" but called method was "%s"},
208             $where, $expectation->method, $actual[0]
209             );
210             }
211             $i++;
212             }
213              
214             return 1;
215             }
216              
217             =head2 verify_ok($callback), verify_ok($mock)
218              
219             Verify and call Test::Builder's ok.
220              
221             With $callback (code reference),
222             the method calls $callback with new mock object.
223              
224             With $mock (not code reference), the method just verify $mock.
225              
226             =cut
227              
228             my $Test = Test::Builder->new;
229              
230             sub verify_ok {
231             my ($self, $arg, $test_name) = @_;
232              
233             $test_name ||= $self->default_test_name;
234              
235             if (ref $arg eq 'CODE') {
236             $Test->ok($self->replay($arg), $test_name);
237             } else {
238             $Test->ok($self->verify($arg), $test_name);
239             }
240             }
241              
242             sub default_test_name {
243             my ($self) = @_;
244              
245             my @methods = map {
246             sprintf(q{'%s'}, $_->method);
247             } @{ $self->_expectations };
248              
249             if (scalar @methods > 1) {
250             my $last = pop @methods;
251             return 'called ' . (join ', ', @methods) . ' and ' . $last;
252             } else {
253             return 'called ' . (join ', ', @methods);
254             }
255             }
256              
257             1;
258             __END__