Actually test that bank account has a balance attr with a trigger
[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     my $ba_meta = BankAccount->meta;
249     ok( $ba_meta->get_attribute('owner')->is_weak_ref,
250         'owner attribute is a weak ref' );
251
252     person06();
253 }
254
255
256 sub has_meta {
257     my $class = shift;
258
259     ok( $class->can('meta'), "$class has a meta() method" )
260         or BAIL_OUT("$class does not have a meta() method (did you forget to 'use Moose'?)");
261 }
262
263 sub check_isa {
264     my $class   = shift;
265     my $parents = shift;
266
267     my @isa = $class->meta->linearized_isa;
268     shift @isa;    # returns $class as the first entry
269
270     my $count = scalar @{$parents};
271     my $noun = PL_N( 'parent', $count );
272
273     is( scalar @isa, $count, "$class has $count $noun" );
274
275     for ( my $i = 0; $i < @{$parents}; $i++ ) {
276         is( $isa[$i], $parents->[$i], "parent[$i] is $parents->[$i]" );
277     }
278 }
279
280 sub has_rw_attr {
281     my $class      = shift;
282     my $name       = shift;
283     my $overridden = shift;
284
285     my $articled = $overridden ? "an overridden $name" : A($name);
286     ok( $class->meta->has_attribute($name),
287         "$class has $articled attribute" );
288
289     my $attr = $class->meta->get_attribute($name);
290
291     is( $attr->get_read_method, $name,
292         "$name attribute has a reader accessor - $name()" );
293     is( $attr->get_write_method, $name,
294         "$name attribute has a writer accessor - $name()" );
295 }
296
297 sub has_ro_attr {
298     my $class = shift;
299     my $name  = shift;
300
301     my $articled = A($name);
302     ok( $class->meta->has_attribute($name),
303         "$class has $articled attribute" );
304
305     my $attr = $class->meta->get_attribute($name);
306
307     is( $attr->get_read_method, $name,
308         "$name attribute has a reader accessor - $name()" );
309     is( $attr->get_write_method, undef,
310         "$name attribute does not have a writer" );
311 }
312
313 sub has_method {
314     my $class = shift;
315     my $name  = shift;
316
317     my $articled = A($name);
318     ok( $class->meta->has_method($name), "$class has $articled method" );
319 }
320
321 sub has_overridden_method {
322     my $class = shift;
323     my $name  = shift;
324
325     my $articled = A($name);
326     ok( $class->meta->has_method($name), "$class has $articled method" );
327
328     my $meth = $class->meta->get_method($name);
329     isa_ok( $meth, 'Moose::Meta::Method::Overridden' );
330 }
331
332 sub has_augmented_method {
333     my $class = shift;
334     my $name  = shift;
335
336     my $articled = A($name);
337     ok( $class->meta->has_method($name), "$class has $articled method" );
338
339     my $meth = $class->meta->get_method($name);
340     isa_ok( $meth, 'Moose::Meta::Method::Augmented' );
341 }
342
343 sub no_droppings {
344     my $class = shift;
345
346     ok( !$class->can('has'), "no Moose droppings in $class" );
347     ok( !$class->can('subtype'), "no Moose::Util::TypeConstraints droppings in $class" );
348 }
349
350 sub is_immutable {
351     my $class = shift;
352
353     ok( $class->meta->is_immutable, "$class has been made immutable" );
354 }
355
356 sub does_role {
357     my $class = shift;
358     my $role  = shift;
359
360     ok( $class->meta->does_role($role), "$class does the $role role" );
361 }
362
363 sub person01 {
364     my $person = Person->new(
365         first_name => 'Bilbo',
366         last_name  => 'Baggins',
367     );
368
369     is( $person->full_name, 'Bilbo Baggins',
370         'full_name() is correctly implemented' );
371
372     $person = Person->new( [ qw( Lisa Smith ) ] );
373     is( $person->first_name, 'Lisa', 'set first_name from two-arg arrayref' );
374     is( $person->last_name, 'Smith', 'set last_name from two-arg arrayref' );
375
376     eval { Person->new( sub { 'foo' } ) };
377     like( $@, qr/\QSingle parameters to new() must be a HASH ref/,
378           'Person constructor still rejects bad parameters' );
379 }
380
381 sub employee01 {
382     my $employee = Employee->new(
383         first_name => 'Amanda',
384         last_name  => 'Palmer',
385         title      => 'Singer',
386     );
387
388     my $called = 0;
389     my $orig_super = \&Employee::super;
390     no warnings 'redefine';
391     local *Employee::super = sub { $called++; goto &$orig_super };
392
393     is( $employee->full_name, 'Amanda Palmer (Singer)',
394         'full_name() is properly overriden in Employee' );
395     ok( $called, 'Employee->full_name calls super()' );
396 }
397
398 sub person02 {
399     my $person = Person->new(
400         first_name => 'Bilbo',
401         last_name  => 'Baggins',
402         balance    => 0,
403     );
404
405     is( $person->as_string, 'Bilbo Baggins',
406         'as_string() is correctly implemented' );
407
408     account_tests($person);
409 }
410
411 sub employee02 {
412     my $employee = Employee->new(
413         first_name => 'Amanda',
414         last_name  => 'Palmer',
415         title      => 'Singer',
416         balance    => 0,
417     );
418
419     is( $employee->as_string, 'Amanda Palmer (Singer)',
420         'as_string() uses overridden full_name method in Employee' );
421
422     account_tests($employee);
423 }
424
425 sub person03 {
426     my $person = Person->new(
427         first_name => 'Bilbo',
428         last_name  => 'Baggins',
429     );
430
431     is( $person->full_name, 'Bilbo Baggins',
432         'full_name() is correctly implemented for a Person without a title' );
433     ok( !$person->has_title,
434         'Person has_title predicate is working correctly (returns false)' );
435
436     $person->title('Ringbearer');
437     ok( $person->has_title, 'Person has_title predicate is working correctly (returns true)' );
438
439     my $called = 0;
440     my $orig_pred = \&Person::has_title;
441     no warnings 'redefine';
442     local *Person::has_title = sub { $called++; goto &$orig_pred };
443
444     is( $person->full_name, 'Bilbo Baggins (Ringbearer)',
445         'full_name() is correctly implemented for a Person with a title' );
446     ok( $called, 'full_name in person uses the predicate for the title attribute' );
447
448     $person->clear_title;
449     ok( !$person->has_title, 'Person clear_title method cleared the title' );
450
451     account_tests( $person, 100 );
452 }
453
454 sub employee03 {
455     my $employee = Employee->new(
456         first_name   => 'Jimmy',
457         last_name    => 'Foo',
458         salary_level => 3,
459         salary       => 42,
460     );
461
462     is( $employee->salary, 30000,
463         'salary is calculated from salary_level, and salary passed to constructor is ignored' );
464 }
465
466 sub person06 {
467     my $person = Person->new(
468         first_name => 'Bilbo',
469         last_name  => 'Baggins',
470     );
471
472     isa_ok( $person->account, 'BankAccount' );
473     is( $person->account->owner, $person,
474         'owner of bank account is person that created account' );
475
476     $person->deposit(10);
477     is_deeply( $person->account->history, [ 100 ],
478                'deposit was recorded in account history' );
479
480     $person->withdraw(15);
481     is_deeply( $person->account->history, [ 100, 110 ],
482                'withdrawal was recorded in account history' );
483
484     $person->withdraw(45);
485     is_deeply( $person->account->history, [ 100, 110, 95 ],
486                'withdrawal was recorded in account history' );
487 }
488
489 sub account_tests {
490     local $Test::Builder::Level = $Test::Builder::Level + 1;
491
492     my $person = shift;
493     my $base_amount = shift || 0;
494
495     $person->deposit(50);
496     eval { $person->withdraw( 75 + $base_amount ) };
497     like( $@, qr/\QBalance cannot be negative/,
498           'cannot withdraw more than is in our balance' );
499
500     $person->withdraw( 23 );
501
502     is( $person->balance, 27 + $base_amount,
503         'balance is 27 (+ starting balance) after deposit of 50 and withdrawal of 23' );
504 }
505
506 1;