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