File Coverage

blib/lib/Test/DBIx/Class/Schema.pm
Criterion Covered Total %
statement 136 142 95.7
branch 37 44 84.0
condition 7 12 58.3
subroutine 13 13 100.0
pod 0 3 0.0
total 193 214 90.1


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