remove extra var declaration
[gitmo/moose-presentations.git] / moose-class / exercises / t / lib / MooseClass / Tests.pm
1 package MooseClass::Tests;
2
3 use strict;
4 use warnings;
5
6 use Lingua::EN::Inflect qw( A PL_N );
7 use Test::More 'no_plan';
8
9 sub tests01 {
10     local $Test::Builder::Level = $Test::Builder::Level + 1;
11
12     has_meta('Person');
13
14     check_isa( 'Person', ['Moose::Object'] );
15
16     has_rw_attr( 'Person', $_ ) for qw( first_name last_name );
17
18     has_method( 'Person', 'full_name' );
19
20     no_droppings('Person');
21     is_immutable('Person');
22
23     person01();
24
25     has_meta('Employee');
26
27     check_isa( 'Employee', [ 'Person', 'Moose::Object' ] );
28
29     has_rw_attr( 'Employee', $_ ) for qw( title salary );
30     has_ro_attr( 'Employee', 'ssn' );
31
32     has_overridden_method( 'Employee', 'full_name' );
33
34     employee01();
35 }
36
37 sub tests02 {
38     tests01();
39
40     local $Test::Builder::Level = $Test::Builder::Level + 1;
41
42     no_droppings($_) for qw( Printable HasAccount );
43
44     does_role( 'Person', $_ ) for qw( Printable HasAccount );
45     has_method( 'Person', $_ ) for qw( as_string deposit withdraw );
46     has_rw_attr( 'Person', 'balance' );
47
48     does_role( 'Employee', $_ ) for qw( Printable HasAccount );
49
50     person02();
51     employee02();
52 }
53
54 sub tests03 {
55     {
56         local $Test::Builder::Level = $Test::Builder::Level + 1;
57
58         has_meta('Person');
59         has_meta('Employee');
60
61         has_rw_attr( 'Person', 'title' );
62
63         has_rw_attr( 'Employee', 'title', 'overridden' );
64         has_rw_attr( 'Employee', 'salary_level' );
65         has_ro_attr( 'Employee', 'salary' );
66
67         has_method( 'Employee', '_build_salary' );
68     }
69
70     ok( ! Employee->meta->has_method('full_name'),
71         'Employee no longer implements a full_name method' );
72
73     my $person_title_attr = Person->meta->get_attribute('title');
74     ok( !$person_title_attr->is_required, 'title is not required in Person' );
75     is( $person_title_attr->predicate, 'has_title',
76         'Person title attr has a has_title predicate' );
77     is( $person_title_attr->clearer, 'clear_title',
78         'Person title attr has a clear_title clearer' );
79
80     my $balance_attr = Person->meta->get_attribute('balance');
81     is( $balance_attr->default, 100, 'balance defaults to 100' );
82
83     my $employee_title_attr = Employee->meta->get_attribute('title');
84     is( $employee_title_attr->default, 'Worker',
85         'title defaults to Worker in Employee' );
86
87     my $salary_level_attr = Employee->meta->get_attribute('salary_level');
88     is( $salary_level_attr->default, 1, 'salary_level defaults to 1' );
89
90     my $salary_attr = Employee->meta->get_attribute('salary');
91     ok( !$salary_attr->init_arg,   'no init_arg for salary attribute' );
92     ok( $salary_attr->has_builder, 'salary attr has a builder' );
93
94     person03();
95     employee03();
96 }
97
98 sub tests04 {
99     {
100         local $Test::Builder::Level = $Test::Builder::Level + 1;
101
102         has_meta('Document');
103         has_meta('Report');
104         has_meta('TPSReport');
105
106         no_droppings('Document');
107         no_droppings('Report');
108         no_droppings('TPSReport');
109
110         has_ro_attr( 'Document',  $_ ) for qw( title author );
111         has_ro_attr( 'Report',    'summary' );
112         has_ro_attr( 'TPSReport', $_ ) for qw( t p s );
113
114         has_method( 'Document', 'output' );
115         has_augmented_method( 'Report', 'output' );
116         has_augmented_method( 'TPSReport', 'output' );
117     }
118
119     my $tps = TPSReport->new(
120         title   => 'That TPS Report',
121         author  => 'Peter Gibbons (for Bill Lumberg)',
122         summary => 'I celebrate his whole collection!',
123         t       => 'PC Load Letter',
124         p       => 'Swingline',
125         s       => 'flair!',
126     );
127
128     my $output = $tps->output;
129     $output =~ s/\n\n+/\n/g;
130
131     is( $output, <<'EOF', 'output returns expected report' );
132 That TPS Report
133 I celebrate his whole collection!
134 t: PC Load Letter
135 p: Swingline
136 s: flair!
137 Written by Peter Gibbons (for Bill Lumberg)
138 EOF
139 }
140
141 sub tests05 {
142     {
143         local $Test::Builder::Level = $Test::Builder::Level + 1;
144
145         has_meta('Person');
146         has_meta('Employee');
147         no_droppings('Employee');
148     }
149
150     for my $attr_name ( qw( first_name last_name title ) ) {
151         my $attr = Person->meta->get_attribute($attr_name);
152
153         ok( $attr->has_type_constraint,
154             "Person $attr_name has a type constraint" );
155         is( $attr->type_constraint->name, 'Str',
156             "Person $attr_name type is Str" );
157     }
158
159     {
160         my $salary_level_attr = Employee->meta->get_attribute('salary_level');
161         ok( $salary_level_attr->has_type_constraint,
162             'Employee salary_level has a type constraint' );
163
164         my $tc = $salary_level_attr->type_constraint;
165
166         for my $invalid ( 0, 11, -14, 'foo', undef ) {
167             my $str = defined $invalid ? $invalid : 'undef';
168             ok( ! $tc->check($invalid),
169                 "salary_level type rejects invalid value - $str" );
170         }
171
172         for my $valid ( 1..10 ) {
173             ok( $tc->check($valid),
174                 "salary_level type accepts valid value - $valid" );
175         }
176     }
177
178     {
179         my $salary_attr = Employee->meta->get_attribute('salary');
180
181         ok( $salary_attr->has_type_constraint,
182             'Employee salary has a type constraint' );
183
184         my $tc = $salary_attr->type_constraint;
185
186         for my $invalid ( 0, -14, 'foo', undef ) {
187             my $str = defined $invalid ? $invalid : 'undef';
188             ok( ! $tc->check($invalid),
189                 "salary type rejects invalid value - $str" );
190         }
191
192         for my $valid ( 1, 100_000, 10**10 ) {
193             ok( $tc->check($valid),
194                 "salary type accepts valid value - $valid" );
195         }
196     }
197
198     {
199         my $ssn_attr = Employee->meta->get_attribute('ssn');
200
201         ok( $ssn_attr->has_type_constraint,
202             'Employee ssn has a type constraint' );
203
204         my $tc = $ssn_attr->type_constraint;
205
206         for my $invalid ( 0, -14, 'foo', undef, '123-ab-1241', '123456789' ) {
207             my $str = defined $invalid ? $invalid : 'undef';
208             ok( ! $tc->check($invalid),
209                 "ssn type rejects invalid value - $str" );
210         }
211
212         for my $valid ( '041-12-1251', '123-45-6789', '926-41-5820' ) {
213             ok( $tc->check($valid),
214                 "ssn type accepts valid value - $valid" );
215         }
216     }
217 }
218
219 sub tests06 {
220     {
221         local $Test::Builder::Level = $Test::Builder::Level + 1;
222
223         has_meta('BankAccount');
224         no_droppings('BankAccount');
225
226         has_rw_attr( 'BankAccount', 'balance' );
227         has_rw_attr( 'BankAccount', 'owner' );
228         has_ro_attr( 'BankAccount', 'history' );
229     }
230
231     my $ba_meta = BankAccount->meta;
232     ok( $ba_meta->has_attribute('balance'),
233         'BankAccount class has a balance attribute' );
234
235     ok( $ba_meta->get_attribute('balance')->has_trigger,
236         'BankAccount balance attribute has a trigger' );
237
238     my $person_meta = Person->meta;
239     ok( ! $person_meta->has_attribute('balance'),
240         'Person class does not have a balance attribute' );
241
242     my $deposit_meth = $person_meta->get_method('deposit');
243     isa_ok( $deposit_meth, 'Moose::Meta::Method::Delegation' );
244
245     my $withdraw_meth = $person_meta->get_method('withdraw');
246     isa_ok( $withdraw_meth, 'Moose::Meta::Method::Delegation' );
247
248     ok( $ba_meta->get_attribute('owner')->is_weak_ref,
249         'owner attribute is a weak ref' );
250
251     person06();
252 }
253
254
255 sub has_meta {
256     my $class = shift;
257
258     ok( $class->can('meta'), "$class has a meta() method" )
259         or BAIL_OUT("$class does not have a meta() method (did you forget to 'use Moose'?)");
260 }
261
262 sub check_isa {
263     my $class   = shift;
264     my $parents = shift;
265
266     my @isa = $class->meta->linearized_isa;
267     shift @isa;    # returns $class as the first entry
268
269     my $count = scalar @{$parents};
270     my $noun = PL_N( 'parent', $count );
271
272     is( scalar @isa, $count, "$class has $count $noun" );
273
274     for ( my $i = 0; $i < @{$parents}; $i++ ) {
275         is( $isa[$i], $parents->[$i], "parent[$i] is $parents->[$i]" );
276     }
277 }
278
279 sub has_rw_attr {
280     my $class      = shift;
281     my $name       = shift;
282     my $overridden = shift;
283
284     my $articled = $overridden ? "an overridden $name" : A($name);
285     ok( $class->meta->has_attribute($name),
286         "$class has $articled attribute" );
287
288     my $attr = $class->meta->get_attribute($name);
289
290     is( $attr->get_read_method, $name,
291         "$name attribute has a reader accessor - $name()" );
292     is( $attr->get_write_method, $name,
293         "$name attribute has a writer accessor - $name()" );
294 }
295
296 sub has_ro_attr {
297     my $class = shift;
298     my $name  = shift;
299
300     my $articled = A($name);
301     ok( $class->meta->has_attribute($name),
302         "$class has $articled attribute" );
303
304     my $attr = $class->meta->get_attribute($name);
305
306     is( $attr->get_read_method, $name,
307         "$name attribute has a reader accessor - $name()" );
308     is( $attr->get_write_method, undef,
309         "$name attribute does not have a writer" );
310 }
311
312 sub has_method {
313     my $class = shift;
314     my $name  = shift;
315
316     my $articled = A($name);
317     ok( $class->meta->has_method($name), "$class has $articled method" );
318 }
319
320 sub has_overridden_method {
321     my $class = shift;
322     my $name  = shift;
323
324     my $articled = A($name);
325     ok( $class->meta->has_method($name), "$class has $articled method" );
326
327     my $meth = $class->meta->get_method($name);
328     isa_ok( $meth, 'Moose::Meta::Method::Overridden' );
329 }
330
331 sub has_augmented_method {
332     my $class = shift;
333     my $name  = shift;
334
335     my $articled = A($name);
336     ok( $class->meta->has_method($name), "$class has $articled method" );
337
338     my $meth = $class->meta->get_method($name);
339     isa_ok( $meth, 'Moose::Meta::Method::Augmented' );
340 }
341
342 sub no_droppings {
343     my $class = shift;
344
345     ok( !$class->can('has'), "no Moose droppings in $class" );
346     ok( !$class->can('subtype'), "no Moose::Util::TypeConstraints droppings in $class" );
347 }
348
349 sub is_immutable {
350     my $class = shift;
351
352     ok( $class->meta->is_immutable, "$class has been made immutable" );
353 }
354
355 sub does_role {
356     my $class = shift;
357     my $role  = shift;
358
359     ok( $class->meta->does_role($role), "$class does the $role role" );
360 }
361
362 sub person01 {
363     my $person = Person->new(
364         first_name => 'Bilbo',
365         last_name  => 'Baggins',
366     );
367
368     is( $person->full_name, 'Bilbo Baggins',
369         'full_name() is correctly implemented' );
370
371     $person = Person->new( [ qw( Lisa Smith ) ] );
372     is( $person->first_name, 'Lisa', 'set first_name from two-arg arrayref' );
373     is( $person->last_name, 'Smith', 'set last_name from two-arg arrayref' );
374
375     eval { Person->new( sub { 'foo' } ) };
376     like( $@, qr/\QSingle parameters to new() must be a HASH ref/,
377           'Person constructor still rejects bad parameters' );
378 }
379
380 sub employee01 {
381     my $employee = Employee->new(
382         first_name => 'Amanda',
383         last_name  => 'Palmer',
384         title      => 'Singer',
385     );
386
387     my $called = 0;
388     my $orig_super = \&Employee::super;
389     no warnings 'redefine';
390     local *Employee::super = sub { $called++; goto &$orig_super };
391
392     is( $employee->full_name, 'Amanda Palmer (Singer)',
393         'full_name() is properly overriden in Employee' );
394     ok( $called, 'Employee->full_name calls super()' );
395 }
396
397 sub person02 {
398     my $person = Person->new(
399         first_name => 'Bilbo',
400         last_name  => 'Baggins',
401         balance    => 0,
402     );
403
404     is( $person->as_string, 'Bilbo Baggins',
405         'as_string() is correctly implemented' );
406
407     account_tests($person);
408 }
409
410 sub employee02 {
411     my $employee = Employee->new(
412         first_name => 'Amanda',
413         last_name  => 'Palmer',
414         title      => 'Singer',
415         balance    => 0,
416     );
417
418     is( $employee->as_string, 'Amanda Palmer (Singer)',
419         'as_string() uses overridden full_name method in Employee' );
420
421     account_tests($employee);
422 }
423
424 sub person03 {
425     my $person = Person->new(
426         first_name => 'Bilbo',
427         last_name  => 'Baggins',
428     );
429
430     is( $person->full_name, 'Bilbo Baggins',
431         'full_name() is correctly implemented for a Person without a title' );
432     ok( !$person->has_title,
433         'Person has_title predicate is working correctly (returns false)' );
434
435     $person->title('Ringbearer');
436     ok( $person->has_title, 'Person has_title predicate is working correctly (returns true)' );
437
438     my $called = 0;
439     my $orig_pred = \&Person::has_title;
440     no warnings 'redefine';
441     local *Person::has_title = sub { $called++; goto &$orig_pred };
442
443     is( $person->full_name, 'Bilbo Baggins (Ringbearer)',
444         'full_name() is correctly implemented for a Person with a title' );
445     ok( $called, 'full_name in person uses the predicate for the title attribute' );
446
447     $person->clear_title;
448     ok( !$person->has_title, 'Person clear_title method cleared the title' );
449
450     account_tests( $person, 100 );
451 }
452
453 sub employee03 {
454     my $employee = Employee->new(
455         first_name   => 'Jimmy',
456         last_name    => 'Foo',
457         salary_level => 3,
458         salary       => 42,
459     );
460
461     is( $employee->salary, 30000,
462         'salary is calculated from salary_level, and salary passed to constructor is ignored' );
463 }
464
465 sub person06 {
466     my $person = Person->new(
467         first_name => 'Bilbo',
468         last_name  => 'Baggins',
469     );
470
471     isa_ok( $person->account, 'BankAccount' );
472     is( $person->account->owner, $person,
473         'owner of bank account is person that created account' );
474
475     $person->deposit(10);
476     is_deeply( $person->account->history, [ 100 ],
477                'deposit was recorded in account history' );
478
479     $person->withdraw(15);
480     is_deeply( $person->account->history, [ 100, 110 ],
481                'withdrawal was recorded in account history' );
482
483     $person->withdraw(45);
484     is_deeply( $person->account->history, [ 100, 110, 95 ],
485                'withdrawal was recorded in account history' );
486 }
487
488 sub account_tests {
489     local $Test::Builder::Level = $Test::Builder::Level + 1;
490
491     my $person = shift;
492     my $base_amount = shift || 0;
493
494     $person->deposit(50);
495     eval { $person->withdraw( 75 + $base_amount ) };
496     like( $@, qr/\QBalance cannot be negative/,
497           'cannot withdraw more than is in our balance' );
498
499     $person->withdraw( 23 );
500
501     is( $person->balance, 27 + $base_amount,
502         'balance is 27 (+ starting balance) after deposit of 50 and withdrawal of 23' );
503 }
504
505 1;