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