File Coverage

blib/lib/Test/DBIx/Class/Schema.pm
Criterion Covered Total %
statement 128 140 91.4
branch 33 44 75.0
condition 8 12 66.6
subroutine 13 13 100.0
pod 0 3 0.0
total 182 212 85.8


line stmt bran cond sub pod time code
1             package Test::DBIx::Class::Schema;
2             {
3             $Test::DBIx::Class::Schema::DIST = 'Test-DBIx-Class-Schema';
4             }
5             $Test::DBIx::Class::Schema::VERSION = '1.0.7';
6             # vim: ts=8 sts=4 et sw=4 sr sta
7 14     14   1997055 use strict;
  14         30  
  14         455  
8 14     13   65 use warnings;
  13         15  
  13         428  
9              
10             # ensure we have "done_testing"
11 13     13   58 use Test::More 0.92;
  13         362  
  13         90  
12              
13             sub new {
14 10     10 0 1088829 my ($proto, $options) = @_;
15 10 50       59 my $self = (defined $options) ? $options : {};
16 10   33     88 bless $self, ref($proto) || $proto;
17 10         29 return $self;
18             }
19              
20             # for populating the correct part of $self
21             sub methods {
22 10     10 0 1484 my ($self, $hashref) = @_;
23              
24 10         52 $self->{methods} = $hashref;
25              
26 10         27 return;
27             }
28              
29             sub run_tests {
30 10     10 0 836 my ($self) = @_;
31 10         17 my ($schema, $rs, $record);
32              
33             # make sure we can use the schema (namespace) module
34 10     9   54 use_ok( $self->{namespace} );
  9         1155  
  9         19  
  9         13  
  9         190  
35              
36             # let users pass in an existing $schema if they (somehow) have one
37 10 50       2522 if (defined $self->{schema}) {
38 10         25 $schema = $self->{schema};
39             }
40             else {
41             # get a schema to query
42 0         0 $schema = $self->{namespace}->connect(
43             $self->{dsn},
44             $self->{username},
45             $self->{password},
46             );
47             }
48 10         46 isa_ok($schema, $self->{namespace});
49              
50             # create a new resultset object and perform tests on it
51             # - this allows us to test ->my_column() without requiring data
52 10         3027 $rs = $schema->resultset( $self->{moniker} );
53 10         437799 $record = $schema->resultset( $self->{moniker} )->new_result({});
54              
55             # make sure our record presents itself as the correct object type
56 10 50       4209 if (defined $self->{glue}) {
57 0         0 isa_ok(
58             $record,
59             $self->{namespace}
60             . '::' . $self->{glue}
61             . '::' . $self->{moniker}
62             );
63             }
64             else {
65             # It looks like the namespace has changed with newer record objects so
66             # that they don't get ::Schema in their name.
67             # So that wew can work with either we now want our record to be the
68             # namespace+moniker with an option '::Schema' in the name.
69             # This means moving away from isa_ok() to like() on a ref()
70 10         184 my $expected_type_re = $self->{namespace} . '::' . $self->{moniker};
71 10         48 $expected_type_re =~ s{::Schema}{(?:::Schema)?};
72 10         254 my $regexp = qr{$expected_type_re};
73 10         69 like(ref($record), $regexp, "The record object is a ::$self->{moniker}");
74             }
75              
76 10         2526 $self->_test_normal_methods($rs);
77 10         42 $self->_test_special_methods($record);
78 10         35 $self->_test_resultset_methods($rs);
79 10         40 $self->_test_unexpected_normal_methods($rs);
80              
81             # TODO: test custom, resultsets
82              
83 10         140 my $tb = Test::More->builder;
84 10 100 100     204 done_testing
85             unless ($tb->{Done_Testing} || $ENV{TEST_AGGREGATE});
86             }
87              
88             sub _test_normal_methods {
89 10     10   22 my $self = shift;
90 10         16 my $rs = shift;
91              
92 10         27 my @std_method_types = qw(columns relations);
93              
94             # 'normal' methods; row & relation
95             # we can try calling these as they gave no side-effects
96 10         13 my @proxied;
97 10         27 foreach my $method_type (@std_method_types) {
98 20         100 SKIP: {
99 20 100       31 if (not @{ $self->{methods}{$method_type} }) {
  20         24  
100 2         9 skip qq{no $method_type methods}, 1;
101             }
102              
103             # try calling each method
104 18         34 METHOD: foreach my $method ( @{ $self->{methods}{$method_type} } ) {
  18         60  
105             # make sure we can call the method
106 49         3394 my $source = $rs->result_source;
107 49         53 my $related_source;
108              
109             # 'normal' relationship
110 49 100       232 if ($source->has_relationship($method)) {
    100          
    50          
111 18         101 eval {
112 18         130 $related_source = $source->related_source($method);
113             };
114 18         2269 is($@, q{}, qq{related source for '$method' exists});
115              
116             # test self.* and foreign.* columns are valid
117 18         4626 my $cond_ref = $source->relationship_info($method)->{cond};
118 18 100       145 $cond_ref = ref $cond_ref eq 'ARRAY' ? $cond_ref : [ $cond_ref ];
119 18         44 COND: foreach my $cond ( @$cond_ref ) {
120             # you can have CODE as the cond_ref - that's unexpected!
121             TODO: {
122 19 50       284 if ('CODE' eq ref($cond)) {
  19         69  
123 0         0 local $TODO = qq{skipping column tests for CODE defined condition};
124 0         0 fail(qq{test '$method' with CODE definition});
125 0         0 next COND;
126             }
127             }
128 19         24 foreach my $foreign_col (keys %{$cond} ) {
  19         65  
129 19         40 my $self_col = $cond->{$foreign_col};
130 19         177 s{^\w+\.}{} for ( $self_col, $foreign_col );
131 19         30 eval {
132 19         73 $source->resultset->slice(0,0)->get_column($self_col)->all;
133             };
134 19         40012 is($@, q{}, qq{self.$self_col valid for '$method' relationship});
135 19         5197 eval {
136 19         82 $related_source->resultset->slice(0,0)->get_column($foreign_col)->all;
137             };
138 19         40107 is($@, q{}, qq{foreign.$foreign_col valid for '$method' relationship});
139             }
140             }
141             }
142              
143             # many_to_many and proxy
144             elsif ( $method_type eq 'relations' ) {
145             # TODO: Factor this out with the same code under proxied
146             # 'columns' accessors
147             RELATIONSHIP:
148 1         11 for my $relationship ( $source->relationships ) {
149 3         13 my $proxy = $source->relationship_info($relationship)->{attrs}{proxy};
150 3 100       16 next RELATIONSHIP if not $proxy;
151 2 50       27 if ( grep m{$method}, @$proxy ) {
152 0         0 pass qq{'$method' relationship exists via proxied relationship '$relationship'};
153 0         0 next METHOD;
154             }
155             }
156 1         4 my $result = $rs->new({});
157             # many_to_many
158 1 50 33     137 if ( $result->can($method)
159             and $result->$method->isa('DBIx::Class::ResultSet') ) {
160 1         3827 pass("'$method' relation is a many-to-many");
161             }
162             else {
163 0         0 fail("'$method' is not a valid relationship" );
164             }
165             }
166              
167             # column accessor
168             elsif ( $method_type eq 'columns' ) {
169 30 100       524 if ( $source->has_column($method) ) {
170 27         308 pass qq{'$method' column defined in result_source};
171 27         6262 eval {
172             # https://rt.cpan.org/Ticket/Display.html?id=65521
173 27         141 my $col = $rs->slice(0,0)->get_column($method)->all;
174             };
175 27         143996 is($@, q{}, qq{'$method' column exists in database});
176 27         8209 next METHOD;
177             }
178             # Proxied columns
179             RELATIONSHIP:
180 3         24 for my $relationship ( $source->relationships ) {
181 7         28 my $proxy = $source->relationship_info($relationship)->{attrs}{proxy};
182 7 100       34 next RELATIONSHIP if not $proxy;
183 4 100       58 if ( grep m{$method}, @$proxy ) {
184 3         17 pass(qq{'$method' column exists via proxied relationship '$relationship'});
185 3         965 next METHOD;
186             }
187             }
188 0         0 fail qq{'$method' column does not exist and is not proxied};
189             }
190             # ... erm ... what's this?
191             else {
192 0         0 die qq{unknown method type: $method_type};
193             }
194             }
195             }
196             } # foreach
197 10         2600 return;
198             }
199              
200             sub _test_special_methods {
201 10     10   60 shift->_test_methods(shift, [qw/custom/]);
202             }
203              
204             sub _test_resultset_methods {
205 10     10   35 shift->_test_methods(shift, [qw/resultsets/]);
206             }
207              
208             sub _test_methods {
209 20     20   29 my $self = shift;
210 20         26 my $thingy = shift;
211 20         24 my $method_types = shift;
212              
213             # 'special' methods; custom
214             # we can't call these as they may have unknown parameters,
215             # side effects, etc
216 20         24 foreach my $method_type (@{ $method_types} ) {
  20         38  
217 19         175 SKIP: {
218 20         20 skip qq{no $method_type methods}, 1
219             unless
220             exists $self->{methods}{$method_type}
221 20 100 100     84 and @{ $self->{methods}{$method_type} };
222 1         7 ok(
223 1         1 @{ $self->{methods}{$method_type} },
224             qq{$method_type list found for testing}
225             );
226             }
227              
228             # call can on each method to make it obvious what's being tested
229 20         2768 foreach my $method (@{ $self->{methods}{$method_type} } ) {
  20         78  
230 1         5 can_ok( $thingy, $method );
231             }
232             } # foreach
233 20         446 return;
234             }
235              
236             sub _test_unexpected_normal_methods {
237 10     10   22 my($self,$rs) = @_;
238 10         37 my $source = $rs->result_source;
239              
240 10         49 my $set = {
241             'columns' => [ $source->columns ],
242             'relations' => [ $source->relationships ],
243             };
244              
245 10         164 foreach my $method_type (sort keys %{$set}) {
  10         60  
246 20         142 my @diff = $self->_diff_arrays(
247             $self->{methods}->{$method_type},
248             $set->{$method_type},
249             );
250              
251 20 100       54 my $plural = (scalar @diff == 1) ? '' : 's';
252 20         74 my $message =
253             qq{'$method_type' method${plural} defined in }
254             . $self->{moniker}
255             . ' but untested: '
256             . join(', ',@diff);
257              
258 20 50       39 if ($self->{test_missing}) {
259 0 0       0 is_deeply(
260             \@diff,
261             [],
262             "All known $method_type method${plural} defined in test"
263             ) || diag $message;
264             }
265             else {
266 20 100       58 if (scalar @diff) {
267 2         9 diag $message;
268             }
269             }
270             }
271             }
272              
273             sub _diff_arrays {
274 20     20   27 my($self,$min,$full) = @_;
275 20         23 my @min = @{$min};
  20         45  
276 20         23 my @full = @{$full};
  20         33  
277              
278 20         29 my %mapped = map{ $_ => 1 } @min;
  49         91  
279 20         53 my @diff = grep (!defined $mapped{$_}, @full);
280              
281 20 50       47 if (wantarray) {
282 20         47 return @diff;
283             }
284 0         0 return \@diff;
285             }
286              
287              
288             1;
289             # ABSTRACT: DBIx::Class schema sanity checking tests
290              
291             __END__
292              
293             =pod
294              
295             =encoding UTF-8
296              
297             =head1 NAME
298              
299             Test::DBIx::Class::Schema - DBIx::Class schema sanity checking tests
300              
301             =head1 VERSION
302              
303             version 1.0.7
304              
305             =head1 SYNOPSIS
306              
307             Create a test script that looks like this:
308              
309             #!/usr/bin/perl
310             # vim: ts=8 sts=4 et sw=4 sr sta
311             use strict;
312             use warnings;
313              
314             # load the module that provides all of the common test functionality
315             use Test::DBIx::Class::Schema;
316              
317             # create a new test object
318             my $schematest = Test::DBIx::Class::Schema->new(
319             {
320             # required
321             dsn => 'dbi:Pg:dbname=mydb', # or use schema option
322             namespace => 'MyDB::Schema',
323             moniker => 'SomeTable',
324             # optional
325             username => 'some_user',
326             password => 'opensesame',
327             glue => 'Result', # fix class name if needed
328             # rather than calling diag will test that all columns/relationships
329             # are accounted for in your test and fail the test if not
330             test_missing => 1,
331             }
332             );
333              
334             # tell it what to test
335             $schematest->methods(
336             {
337             columns => [
338             qw[
339             id
340             column1
341             column2
342             columnX
343             foo_id
344             ]
345             ],
346              
347             relations => [
348             qw[
349             foo
350             ]
351             ],
352              
353             custom => [
354             qw[
355             some_method
356             ]
357             ],
358              
359             resultsets => [
360             qw[
361             ]
362             ],
363             }
364             );
365              
366             # run the tests
367             $schematest->run_tests();
368              
369             Run the test script:
370              
371             prove -l t/schematest/xx.mydb.t
372              
373             =head2 Options
374              
375             Either C<dsn> (eg C<dbi:Pg:dbname=mydb>) or C<schema> (an already
376             created schema object) must be set.
377              
378             If the database requires credentials, set C<username> and C<password>.
379              
380             C<namespace>, C<glue> and C<moniker> define the class being tested.
381             For example, if your class is C<MyDB::Schema::Result::SomeTable> then use:
382              
383             namespace => 'MyDB::Schema',
384             glue => 'Result,
385             moniker => 'SomeTable',
386              
387             C<glue> is not required if the combination of C<namespace> and C<moniker>
388             is enough to define the class, e.g. C<MyDB::Schema::SomeTable>.
389              
390             =head2 done_testing
391              
392             Under normal circumstances there is no need to add C<done_testing> to your
393             test script; it's automatically called at the end of C<run_tests()> I<unless>
394             you are running tests under L<Test::Aggregate>.
395              
396             If you are running aggregated tests you will need to add
397              
398             done_testing;
399              
400             to your top-level script.
401              
402             =head1 DESCRIPTION
403              
404             It's really useful to be able to test and confirm that DBIC classes have and
405             support a known set of methods.
406              
407             Testing these one-by-one is more than tedious and likely to discourage you
408             from writing the relevant test scripts.
409              
410             As a lazy person myself I don't want to write numerous near-identical scripts.
411              
412             Test::DBIx::Class::Schema takes the copy-and-paste out of DBIC schema class testing.
413              
414             =head1 SEE ALSO
415              
416             L<DBIx::Class>,
417             L<Test::More>,
418             L<Test::Aggregate>
419              
420             =head1 CONTRIBUTORS
421              
422             Gianni Ceccarelli C<< <dakkar@thenautilus.net> >>,
423             Darius Jokilehto,
424             Jason Tang C<< <tang.jason.ch@gmail.com> >>,
425             Rupert Lane (L<https://github.com/rupertl>)
426              
427             =head1 AUTHOR
428              
429             Chisel Wright <chisel@chizography.net>
430              
431             =head1 COPYRIGHT AND LICENSE
432              
433             This software is copyright (c) 2015 by Chisel Wright.
434              
435             This is free software; you can redistribute it and/or modify it under
436             the same terms as the Perl 5 programming language system itself.
437              
438             =cut