File Coverage

blib/lib/Test/DBIx/Class/Schema.pm
Criterion Covered Total %
statement 140 146 95.8
branch 38 46 82.6
condition 14 18 77.7
subroutine 13 13 100.0
pod 0 3 0.0
total 205 226 90.7


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.12';
7             # vim: ts=8 sts=4 et sw=4 sr sta
8 15     15   2849315 use strict;
  15         43  
  15         388  
9 15     14   90 use warnings;
  14         36  
  14         489  
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   81 use Test::More 1.302015;
  14         397  
  14         129  
15              
16             sub new {
17 12     12 0 1419806 my ($proto, $options) = @_;
18 12 100       63 my $self = (defined $options) ? $options : {};
19 12   33     105 bless $self, ref($proto) || $proto;
20 12         48 return $self;
21             }
22              
23             # for populating the correct part of $self
24             sub methods {
25 11     11 0 3215 my ($self, $hashref) = @_;
26              
27 11         61 $self->{methods} = $hashref;
28              
29 11         38 return;
30             }
31              
32             sub run_tests {
33 11     11 0 5120 my ($self) = @_;
34 11         33 my ($schema, $rs, $record);
35              
36             # make sure we can use the schema (namespace) module
37 11     10   73 use_ok( $self->{namespace} );
  10         2262  
  10         33  
  10         38  
  10         219  
38              
39             # let users pass in an existing $schema if they (somehow) have one
40 11 100       3778 if (defined $self->{schema}) {
41 10         51 $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         63 );
50             }
51 11         5449 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         4619 $rs = $schema->resultset( $self->{moniker} );
56 11         497233 $record = $schema->resultset( $self->{moniker} )->new_result({});
57              
58             # make sure our record presents itself as the correct object type
59 11 50       5850 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         428 my $expected_type_re = $self->{namespace} . '::' . $self->{moniker};
74 11         61 $expected_type_re =~ s{::Schema}{(?:::Schema)?};
75 11         276 my $regexp = qr{$expected_type_re};
76 11         89 like(ref($record), $regexp, "The record object is a ::$self->{moniker}");
77             }
78              
79 11         3678 $self->_test_normal_methods($rs);
80 11         65 $self->_test_special_methods($record);
81 11         54 $self->_test_resultset_methods($rs);
82 11         61 $self->_test_unexpected_normal_methods($rs);
83              
84             # TODO: test custom, resultsets
85              
86 11         1577 my $ctx = Test::More->builder->ctx;
87             my $tb2_already_done_testing =
88 11         1061 defined $ctx->snapshot->hub->meta('Test::Builder')->{Done_Testing};
89 11         327 $ctx->release;
90             done_testing
91 11 100 100     438 unless ($tb2_already_done_testing || $ENV{TEST_AGGREGATE});
92             }
93              
94             sub _test_normal_methods {
95 11     11   31 my $self = shift;
96 11         27 my $rs = shift;
97              
98 11         41 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         28 my @proxied;
103 11         34 foreach my $method_type (@std_method_types) {
104             SKIP: {
105 22 100 100     690 if (not exists $self->{methods}{$method_type} or not @{ $self->{methods}{$method_type} }) {
  22         124  
  21         137  
106 4         25 skip qq{no $method_type methods}, 1;
107             }
108              
109             # try calling each method
110 18         47 METHOD: foreach my $method ( @{ $self->{methods}{$method_type} } ) {
  18         67  
111             # make sure we can call the method
112 51         6681 my $source = $rs->result_source;
113 51         108 my $related_source;
114              
115             # 'normal' relationship
116 51 100       339 if ($source->has_relationship($method)) {
    100          
    50          
117 22         204 eval {
118 22         189 $related_source = $source->related_source($method);
119             };
120 22         3917 is($@, q{}, qq{related source for '$method' exists});
121              
122             # test self.* and foreign.* columns are valid
123 22         7517 my $cond_ref = $source->relationship_info($method)->{cond};
124 22 100       249 $cond_ref = ref $cond_ref eq 'ARRAY' ? $cond_ref : [ $cond_ref ];
125 22         80 COND: foreach my $cond ( @$cond_ref ) {
126             # you can have CODE as the cond_ref - that's unexpected!
127             TODO: {
128 23 100       556 if ('CODE' eq ref($cond)) {
  23         129  
129 3         9 local $TODO = qq{skipping column tests for CODE defined condition};
130 3         24 fail(qq{test '$method' with CODE definition});
131 3         3618 next COND;
132             }
133             }
134 20         51 foreach my $foreign_col (keys %{$cond} ) {
  20         92  
135 20         70 my $self_col = $cond->{$foreign_col};
136 20         226 s{^\w+\.}{} for ( $self_col, $foreign_col );
137 20         64 eval {
138 20         107 $source->resultset->slice(0,0)->get_column($self_col)->all;
139             };
140 20         73400 is($@, q{}, qq{self.$self_col valid for '$method' relationship});
141 20         7998 eval {
142 20         133 $related_source->resultset->slice(0,0)->get_column($foreign_col)->all;
143             };
144 20         64859 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         23 for my $relationship ( $source->relationships ) {
155 6         35 my $proxy = $source->relationship_info($relationship)->{attrs}{proxy};
156 6 100       38 next RELATIONSHIP if not $proxy;
157 3 100       48 if ( grep m{$method}, @$proxy ) {
158 1         9 pass qq{'$method' relationship exists via proxied relationship '$relationship'};
159 1         327 next METHOD;
160             }
161             }
162 1         5 my $result = $rs->new({});
163             # many_to_many
164 1 50 33     161 if ( $result->can($method)
165             and $result->$method->isa('DBIx::Class::ResultSet') ) {
166 1         4585 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 27 100       574 if ( $source->has_column($method) ) {
176 24         475 pass qq{'$method' column defined in result_source};
177 24         7115 eval {
178             # https://rt.cpan.org/Ticket/Display.html?id=65521
179 24         160 my $col = $rs->slice(0,0)->get_column($method)->all;
180             };
181 24         152821 is($@, q{}, qq{'$method' column exists in database});
182 24         9009 next METHOD;
183             }
184             # Proxied columns
185             RELATIONSHIP:
186 3         28 for my $relationship ( $source->relationships ) {
187 4         33 my $proxy = $source->relationship_info($relationship)->{attrs}{proxy};
188 4 50       24 next RELATIONSHIP if not $proxy;
189 4 100       48 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         2799 return;
204             }
205              
206             sub _test_special_methods {
207 11     11   67 shift->_test_methods(shift, [qw/custom/]);
208             }
209              
210             sub _test_resultset_methods {
211 11     11   50 shift->_test_methods(shift, [qw/resultsets/]);
212             }
213              
214             sub _test_methods {
215 22     22   53 my $self = shift;
216 22         50 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         45 foreach my $method_type (@{ $method_types} ) {
  22         57  
223             SKIP: {
224 22         40 skip qq{no $method_type methods}, 1
225             unless
226             exists $self->{methods}{$method_type}
227 22 100 100     118 and @{ $self->{methods}{$method_type} };
  21         207  
228             ok(
229 1         2 @{ $self->{methods}{$method_type} },
  1         7  
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         9423 foreach my $method (@{ $self->{methods}{$method_type} } ) {
  22         97  
236 1         7 can_ok( $thingy, $method );
237             }
238             } # foreach
239 22         370 return;
240             }
241              
242             sub _test_unexpected_normal_methods {
243 11     11   38 my($self,$rs) = @_;
244 11         55 my $source = $rs->result_source;
245              
246 11         67 my $set = {
247             'columns' => [ $source->columns ],
248             'relations' => [ $source->relationships ],
249             };
250              
251 11         247 foreach my $method_type (sort keys %{$set}) {
  11         68  
252             SKIP: {
253 22 100 100     854 if (not exists $self->{methods}->{$method_type} or not @{ $self->{methods}->{$method_type} }) {
  22         95  
  21         111  
254 4         18 skip qq{no $method_type methods}, 1;
255             }
256              
257             my @diff = $self->_diff_arrays(
258             $self->{methods}->{$method_type},
259 18         82 $set->{$method_type},
260             );
261            
262 18 100       69 my $plural = (scalar @diff == 1) ? '' : 's';
263             my $message =
264             qq{'$method_type' method${plural} defined in }
265             . $self->{moniker}
266 18         93 . ' but untested: '
267             . join(', ',@diff);
268            
269 18 50       61 if ($self->{test_missing}) {
270 0 0       0 is_deeply(
271             \@diff,
272             [],
273             "All known $method_type method${plural} defined in test"
274             ) || diag $message;
275             }
276             else {
277 18 100       68 if (scalar @diff) {
278 4         20 diag $message;
279             }
280             }
281             }
282             }
283             }
284              
285             sub _diff_arrays {
286 18     18   54 my($self,$min,$full) = @_;
287 18         36 my @min = @{$min};
  18         63  
288 18         36 my @full = @{$full};
  18         47  
289              
290 18         43 my %mapped = map{ $_ => 1 } @min;
  51         141  
291 18         72 my @diff = grep (!defined $mapped{$_}, @full);
292              
293 18 50       73 if (wantarray) {
294 18         73 return @diff;
295             }
296 0         0 return \@diff;
297             }
298              
299              
300             1;
301             # ABSTRACT: DBIx::Class schema sanity checking tests
302              
303             __END__
304              
305             =pod
306              
307             =encoding UTF-8
308              
309             =head1 NAME
310              
311             Test::DBIx::Class::Schema - DBIx::Class schema sanity checking tests
312              
313             =head1 VERSION
314              
315             version 1.0.12
316              
317             =head1 SYNOPSIS
318              
319             Create a test script that looks like this:
320              
321             #!/usr/bin/perl
322             # vim: ts=8 sts=4 et sw=4 sr sta
323             use strict;
324             use warnings;
325              
326             # load the module that provides all of the common test functionality
327             use Test::DBIx::Class::Schema;
328              
329             # create a new test object
330             my $schematest = Test::DBIx::Class::Schema->new(
331             {
332             # required
333             dsn => 'dbi:Pg:dbname=mydb', # or use schema option
334             namespace => 'MyDB::Schema',
335             moniker => 'SomeTable',
336             # optional
337             username => 'some_user',
338             password => 'opensesame',
339             glue => 'Result', # fix class name if needed
340             # rather than calling diag will test that all columns/relationships
341             # are accounted for in your test and fail the test if not
342             test_missing => 1,
343             }
344             );
345              
346             # tell it what to test
347             $schematest->methods(
348             {
349             columns => [
350             qw[
351             id
352             column1
353             column2
354             columnX
355             foo_id
356             ]
357             ],
358              
359             relations => [
360             qw[
361             foo
362             ]
363             ],
364              
365             custom => [
366             qw[
367             some_method
368             ]
369             ],
370              
371             resultsets => [
372             qw[
373             ]
374             ],
375             }
376             );
377              
378             # run the tests
379             $schematest->run_tests();
380              
381             Run the test script:
382              
383             prove -l t/schematest/xx.mydb.t
384              
385             =head2 Options
386              
387             Either C<dsn> (eg C<dbi:Pg:dbname=mydb>) or C<schema> (an already
388             created schema object) must be set.
389              
390             If the database requires credentials, set C<username> and C<password>.
391              
392             C<namespace>, C<glue> and C<moniker> define the class being tested.
393             For example, if your class is C<MyDB::Schema::Result::SomeTable> then use:
394              
395             namespace => 'MyDB::Schema',
396             glue => 'Result,
397             moniker => 'SomeTable',
398              
399             C<glue> is not required if the combination of C<namespace> and C<moniker>
400             is enough to define the class, e.g. C<MyDB::Schema::SomeTable>.
401              
402             =head2 done_testing
403              
404             Under normal circumstances there is no need to add C<done_testing> to your
405             test script; it's automatically called at the end of C<run_tests()> I<unless>
406             you are running tests under L<Test::Aggregate>.
407              
408             If you are running aggregated tests you will need to add
409              
410             done_testing;
411              
412             to your top-level script.
413              
414             If you are running under L<Test::Class::Moose> or L<Test::Class> you will need to
415             disable this behaviour manually as there is no way to detect it. To do that,
416             set C<$ENV{TEST_AGGREGATE} = 1> before calling C<run_tests> or your test suite
417             might blow up.
418              
419             =head1 DESCRIPTION
420              
421             It's really useful to be able to test and confirm that DBIC classes have and
422             support a known set of methods.
423              
424             Testing these one-by-one is more than tedious and likely to discourage you
425             from writing the relevant test scripts.
426              
427             As a lazy person myself I don't want to write numerous near-identical scripts.
428              
429             Test::DBIx::Class::Schema takes the copy-and-paste out of DBIC schema class testing.
430              
431             =head1 SEE ALSO
432              
433             L<DBIx::Class>,
434             L<Test::More>,
435             L<Test::Aggregate>
436              
437             =begin markdown
438              
439             ## BUILD STATUS
440              
441             ### master
442              
443             [![Build Status](https://travis-ci.org/chiselwright/test-dbix-class-schema.svg?branch=master)](https://travis-ci.org/chiselwright/test-dbix-class-schema)
444              
445             =end markdown
446              
447             =head1 AUTHOR
448              
449             Chisel Wright <chisel@chizography.net>
450              
451             =head1 COPYRIGHT AND LICENSE
452              
453             This software is copyright (c) 2017 by Chisel Wright.
454              
455             This is free software; you can redistribute it and/or modify it under
456             the same terms as the Perl 5 programming language system itself.
457              
458             =head1 CONTRIBUTORS
459              
460             =for stopwords Darius Jokilehto Dave Cross Jason Tang Rupert Lane simbabque
461              
462             =over 4
463              
464             =item *
465              
466             Darius Jokilehto <darius.jokilehto@net-a-porter.com>
467              
468             =item *
469              
470             Dave Cross <davidc@broadbean.com>
471              
472             =item *
473              
474             Jason Tang <tang.jason.ch@gmail.com>
475              
476             =item *
477              
478             Rupert Lane <rupert@rupert-lane.org>
479              
480             =item *
481              
482             simbabque <simbabque@cpan.org>
483              
484             =back
485              
486             =cut