File Coverage

lib/Test/DBIC/ExpectedQueries.pm
Criterion Covered Total %
statement 161 173 93.0
branch 30 42 71.4
condition 10 16 62.5
subroutine 25 27 92.5
pod 3 8 37.5
total 229 266 86.0


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Test::DBIC::ExpectedQueries - Test that only expected DBIx::Class queries are run
4              
5             =head1 VERSION 2.000
6              
7             Version 2.000 is out with a breaking change. If you're having issues
8             with your test suite, please see the L<Changes> file for details.
9              
10              
11             =head1 DESCRIPTION
12              
13             Ensure that only the DBIx::Class SQL queries you expect are executed
14             while a particular piece of code under test is run. Find the places in
15             your code where the unexpected queries are executed.
16              
17              
18             =head2 Avoiding the n+1 problem
19              
20             When following a relation off a DBIC row object it's easy to overlook
21             the fact that it might be causing one query for each and every row in
22             the resultset. This can easily be solved by prefetching those
23             relations, but you have to know it happens first.
24              
25             This module will help you finding unexpected queries, where they are
26             being caused, and to ensure you don't accidentally start running many
27             single-row queries in the future.
28              
29              
30              
31             =head1 SYNOPSIS
32              
33             =head2 Setup
34              
35             use Test::More;
36             use Test::DBIC::ExpectedQueries;
37             my $schema = ...; # Connect to a DBIx::Class schema
38              
39             =head2 Simple
40              
41             my @book_rows = expected_queries(
42             $schema,
43             sub {
44             $schema->resultset("Book")->find(34);
45             $schema->resultset("Author")->create( ... );
46             $schema->resultset("Book")->search( undef, { join => "author" } )->all;
47             },
48             {
49             book => {
50             select => "<= 2",
51             stack_trace => 1,
52             },
53             author => { insert => undef },
54             },
55             "Doing that stuff runs correct SQL", # optional
56             );
57              
58              
59             =head2 Flexible
60              
61             my $queries = Test::DBIC::ExpectedQueries->new({
62             schema => $schema,
63             report_subselect_tables => 1,
64             });
65             $queries->run(sub {
66             $schema->resultset("Book")->find(34);
67             $schema->resultset("Author")->create( ... );
68             });
69             my @book_rows = $queries->run(sub {
70             $schema->resultset("Book")->search( undef, { join => "author" } )->all;
71             });
72              
73             $queries->test({
74             book => { select => "<= 2"},
75             author => { insert => undef },
76             });
77              
78             # or, with test description
79             $queries->test(
80             {
81             book => { select => "<= 2"},
82             author => { insert => undef },
83             },
84             "Doing that stuff runs correct SQL", # optional
85             );
86              
87              
88              
89             =head1 USAGE
90              
91             You might already have a good idea of what queries are/should be
92             run. But often that's not the case.
93              
94             Start by wrapping some DBIC application code in a test without any
95             specific limits. The default expectation for all tables is 0 queries
96             run. So the test will fail, and report all the executed queries it
97             didn't expect.
98              
99             Now you know what's going on. Now you can add prefetches or caching
100             for queries that shouldn't happen and specify query limits for the
101             currently known behaviour.
102              
103             Whether you want to nail down the expected queries with exact counts,
104             or just put wide-margin comparisons in place is up to you.
105              
106              
107             =head2 Finding the unexpected queries
108              
109             Once you find unexpected queries made by your code, the next step is
110             eliminating them. But where are they called from?
111              
112              
113             =head3 Chained ResultSets
114              
115             DBIC has this nice feature of chaining resultsets, which means you can
116             create a resultset and later modify it by adding things to the WHERE
117             clause, joining in other resultsets, add prefetching of relations or
118             whatever you need to do.
119              
120             You can create small logical pieces of queries (and put them on their
121             corresponding Result/ResultSet classes) and then combine them in to
122             actual queries, expressed in higher level operation. This is very,
123             very powerful and one of the coolest features of DBIC.
124              
125             There is a problem with passing around a resultset before finally
126             executing it though, and that is that it can often be tricky to find
127             exactly where it is being executed.
128              
129             =head3 Following relations
130              
131             The problem of finding the source of a database call isn't limited to
132             chained queries though. The same thing happens when you construct a
133             query, and then follow relations off of the main table. This is what
134             causes the n + 1 problem and you accidentally make n queries for
135             individual rows on top of the first one.
136              
137             These additional queries might be a long way off from where the
138             initial query was made.
139              
140              
141             =head3 Show the stack trace
142              
143             To solve this problem of where the queries originate you can tell
144             Test::DBIC::ExpectedQueries to show a C<stack_trace> for particular
145             tables.
146              
147             These call stacks may be quite deep, so you'll have to find the
148             unexpected queries first, and then enable the call stack for each of
149             them. That will also avoid spamming the test output with things you're
150             not interested in.
151              
152              
153             =head2 Return value from the test
154              
155             For the subroutine C<expected_queries(...)>, and the method
156             C<$queries->run(...)>, the return value is whatever the subroutine
157             under test returned, so it's easy to wrap the DBIC code under test and
158             still get out the result.
159              
160             It is context sensitive.
161              
162              
163             =head2 Executed queries vs resultsets
164              
165             Only queries actually executed inside the test are being
166             monitored. This sounds obvious, but might be a source of problems.
167              
168             Many DBIC methods are context sensitive, and in scalar context might
169             just return an unrealized resultset rather than execute a query and
170             return the resulting rows. If you're unsure, assigning the query to an
171             array will make it run in list context and therefore execute the SQL
172             query. Or you can call C<-&gt;>all> on the resultset object.
173              
174              
175             =head2 DBIC_TRACE
176              
177             Normally, setting the ENV variable DBIC_TRACE can be used to "warn"
178             the DBIC queries.
179              
180             Test::DBIC:ExpectedQueries uses the same mechanism as DBIC_TRACE does,
181             so while the code is run under the test the normal DBIC_TRACE will not
182             happen.
183              
184              
185              
186             =head1 SUBROUTINES
187              
188             =head2 expected_queries( $schema, $sub_ref, $expected_table_operations = {}, $description? ) : $result | @result
189              
190             Run $sub_ref and collect stats for queries executed on $schema, then
191             test (using $description) that they match the
192             $expected_table_operations.
193              
194             Return the return value of $sub_ref->().
195              
196             See the ANNOTATED EXAMPLES below for examples on how the
197             $expected_table_operations is used, but here's a simple example:
198              
199             {
200             book => { select => "<= 2", update => 3 },
201             author => { insert => undef },
202             genre => { select => 2, stack_trace => 1 },
203             },
204              
205              
206             =over 4
207              
208             =item *
209              
210             Use table names as found in the raw SQL, not DBIC terms like resultset
211             and relation names. For relational queries, only the first main table
212             is collected.
213              
214             =item *
215              
216             Use SQL terms like "select", "insert", "update", "delete", not DBIC
217             terms like "create" and "search".
218              
219             =item *
220              
221             A number means exact match. Comparisons in a string means, well that.
222              
223             =item *
224              
225             Undef means any number of queries
226              
227             =item *
228              
229             If you need to see where the queries for a table are executed from,
230             use C<stack_trace => 1>.
231              
232             =back
233              
234              
235              
236             =head1 METHODS
237              
238             =head2 new({ schema => $schema, report_subselect_tables => 0 }}) : $new_object
239              
240             Create new test object.
241              
242             $schema is a DBIx::Class::Schema object.
243              
244             If C<report_subselect_tables> is false (default), any SQL query like
245              
246             select * from (select abc from def);
247              
248             will report a select on the table C<select>. However, if you specify
249             C<report_subselect_tables>, it will try to find the C<def> table
250             inside the subselect.
251              
252              
253             =head2 run( $sub_ref ) : $result | @result
254              
255             Run $sub_ref->() and collect all DBIC queries being run.
256              
257             Return the return value of $sub_ref->().
258              
259             You can call $queries->run() multiple times to add to the collected
260             stats before finally calling $queries->test().
261              
262              
263             =head2 test( $expected_table_operations = {}, $description? ) : $is_passing
264              
265             Test (using $description) the collected queries against
266             $expected_table_operations (see above) and either pass or fail a
267             Test::More test.
268              
269             If the test fails, C<diag> all queries relating to the tables with
270             unexpected activity.
271              
272             If anything failed to be identified as a known query, always C<note>
273             those queries. But don't fail the test just because of it.
274              
275             Reset the collected stats, so subsequent calls to ->run() start with a
276             clean slate.
277              
278              
279              
280             =head1 ANNOTATED EXAMPLES
281              
282             =head2 Simple interface
283              
284             use Test::More;
285             use Test::DBIC::ExpectedQueries;
286              
287             my $schema = ...; # A DBIx::Class schema object
288              
289             # The return value of the subref is returned
290             my $author_rows = expected_queries(
291             # Collect stats for this schema
292             $schema,
293             # when running this code
294             sub {
295             $author_tree->create_authors_for_tabs($schema),
296             },
297             # and ensure these are the expected queries
298             {
299             # For the "tree_node" table
300             tree_node => {
301             update => ">= 1", # Number of updates must be >= 1
302             select => undef, # Any number of selects are fine
303             },
304             # For the "author" table
305             author => {
306             update => 8, # Number of updates must be exactly 8
307             stack_trace => 1, # Show stack trace if it fails
308             },
309             user_session => {
310             delete => "< 10", # No more than 9 deletes allowed
311             },
312             # Any query on any other table will fail the test
313             },
314             );
315              
316              
317             =head2 Flexible interface
318              
319             Using the OO interface allows you to collect stats for many separate
320             queries.
321              
322             It is also useful for when you care about individual return values
323             from methods called, and when you don't know the expected number of
324             queries until after they have been run.
325              
326             use Test::More;
327             use Test::DBIC::ExpectedQueries;
328              
329             my $queries = Test::DBIC::ExpectedQueries->new({ schema => $schema });
330             my $author_rows = $queries->run(
331             sub { $author_tree->create_authors_for_tabs($schema) },
332             );
333              
334             # Add more stats in a second run
335             $queries->run( sub { $author_tree->check_stuff() } );
336              
337             # ... test other things
338              
339             my $total_author_count = @{$author_rows} + 1; # or whatever
340              
341             # This resets the collected stats
342             $queries->test(
343             {
344             author => {
345             insert => $total_author_count,
346             update => undef,
347             },
348             field => { select => "<= 1" },
349             tree_node => { select => 2 },
350             },
351             );
352              
353             =cut
354              
355             package Test::DBIC::ExpectedQueries;
356             $Test::DBIC::ExpectedQueries::VERSION = '2.002';
357 4     4   327558 use Moo;
  4         29658  
  4         21  
358 4     4   6987 use Exporter::Tiny;
  4         13394  
  4         25  
359 4     4   516 BEGIN {extends "Exporter::Tiny"};
360             our @EXPORT = "expected_queries";
361              
362              
363 4     4   90033 use Test::More;
  4         9  
  4         27  
364 4     4   3398 use Try::Tiny;
  4         5595  
  4         221  
365 4     4   32 use Carp;
  4         9  
  4         171  
366 4     4   2045 use DBIx::Class;
  4         160064  
  4         135  
367 4     4   2152 use Devel::StackTrace;
  4         13557  
  4         172  
368 4     4   2576 use autobox::Core;
  4         74752  
  4         27  
369 4     4   5321 use autobox::Transform;
  4         139461  
  4         26  
370              
371 4     4   6659 use Test::DBIC::ExpectedQueries::Query;
  4         11  
  4         9429  
372              
373              
374              
375             ### Simple procedural interface
376              
377             sub expected_queries {
378 1     1 1 148 my ($schema, $subref, $expected, $description) = @_;
379 1   50     4 $expected ||= {};
380 1         2 local $Test::Builder::Level = $Test::Builder::Level + 1;
381              
382 1         12 my $queries = Test::DBIC::ExpectedQueries->new({ schema => $schema });
383              
384 1         1804 my $return_values;
385 1 50       4 if (wantarray()) {
386 0         0 $return_values = [ $queries->run($subref) ];
387             }
388             else {
389 1         5 $return_values = [ scalar $queries->run($subref) ];
390             }
391              
392 1         4 $queries->test($expected, $description);
393              
394 1 50       2 return @$return_values if wantarray();
395 1         17 return $return_values->[0];
396             }
397              
398              
399              
400             ### Full OO interface
401              
402             has schema => (
403             is => "ro",
404             required => 1,
405             );
406              
407             has report_subselect_tables => (
408             is => "ro",
409             default => sub { 0 },
410             lazy => 1,
411             );
412              
413             has queries => (
414             is => "rw",
415             default => sub { [] },
416             trigger => sub { shift->clear_table_operation_count },
417             lazy => 1,
418             clearer => 1,
419             );
420              
421             has table_operation_count => (
422             is => "lazy",
423             clearer => 1,
424             );
425             sub _build_table_operation_count {
426 6     6   86 my $self = shift;
427              
428 6         9 my $table_operation_count = {};
429 6         10 for my $query (grep { $_->operation } @{$self->queries}) {
  37         86  
  6         96  
430 33         74 $table_operation_count->{ $query->table }->{ $query->operation }++;
431             }
432              
433 6         32 return $table_operation_count;
434             }
435              
436             has ignore_classes => ( is => "lazy" );
437             sub _build_ignore_classes {
438 1     1   13 my $self = shift;
439             return [
440             # "main",
441 1         15 "Class::MOP::Method::Wrapped",
442             "Context::Preserve",
443             "DBIx::Class",
444             "DBIx::Class::ResultSet",
445             "DBIx::Class::Row",
446             "DBIx::Class::Row",
447             "DBIx::Class::Schema",
448             "DBIx::Class::Storage::BlockRunner",
449             "DBIx::Class::Storage::DBI",
450             "DBIx::Class::Storage::Statistics",
451             "Mojo::IOLoop",
452             "Mojo::Promise",
453             "Mojo::Reactor",
454             "Moose::Meta::Method::Delegation",
455             "Test::Builder",
456             "Test::Builder",
457             "Test::Class",
458             "Test::Class::Moose",
459             "Test::Class::Moose::Executor::Sequential",
460             "Test::Class::Moose::Report::Method",
461             "Test::Class::Moose::Role::Executor",
462             "Test::Class::Moose::Runner",
463             "Test::DBIC::ExpectedQueries",
464             "Test::More",
465             "Try::Tiny",
466             "Try::Tiny::Catch",
467             ];
468             }
469              
470             sub _stack_trace {
471 1     1   3134 my $self = shift;
472              
473 1         28 my $trace = Devel::StackTrace->new(
474             message => "SQL executed",
475             ignore_class => $self->ignore_classes,
476             );
477 1         606 my $callers = $trace->as_string;
478              
479 1         1708 $callers =~ s/=?(HASH|ARRAY|CODE|GLOB)\(0x\w+\)/<$1>/gsm;
480              
481             # Indent all but first line
482 1         11 my ($first, @rest) = $callers->split(qr/\n/);
483             my $rest = @rest
484             ->filter
485 3     3   71 ->map(sub { " $_" })
486 1         21 ->join("\n");
487 1         34 $callers = "$first\n$rest";
488              
489 1         16 return $callers;
490             }
491              
492             sub run {
493 1     1 1 2 my $self = shift;
494 1         16 my ($subref) = @_;
495 1         3 my $wantarray = wantarray(); # Avoid it being masked in side try-catch block
496              
497 1         6 my $storage = $self->schema->storage;
498              
499 1         7 my $previous_debug = $storage->debug();
500 1         5 $storage->debug(1);
501              
502 1         3 my @queries;
503 1         4 my $previous_callback = $storage->debugcb();
504             $storage->debugcb( sub {
505 0     0   0 my ($op, $sql) = @_;
506             ###JPL: don't ignore the $op, use it instead of parsing out
507             ###the operation?
508 0         0 chomp($sql);
509 0         0 push(
510             @queries,
511             Test::DBIC::ExpectedQueries::Query->new({
512             sql => $sql,
513             stack_trace => $self->_stack_trace(),
514             report_subselect_tables => $self->report_subselect_tables,
515             }),
516             );
517 1         13 } );
518              
519 1         8 my $return_values;
520             try {
521 1 50   1   102 if ($wantarray) {
522 0         0 $return_values = [ $subref->() ];
523             }
524             else {
525 1         2 $return_values = [ scalar $subref->() ];
526             }
527             }
528 0     0   0 catch { die($_) }
529             finally {
530 1     1   44 $storage->debugcb($previous_callback);
531 1         3 $storage->debug($previous_debug);
532 1         10 };
533              
534 1         22 $self->queries([ @{$self->queries}, @queries ]);
  1         26  
535              
536 1 50       12 return @$return_values if $wantarray;
537 1         3 return $return_values->[0];
538             }
539              
540             sub test {
541 1     1 1 2 my $self = shift;
542 1         2 my ($expected, $test_description) = @_;
543 1   50     3 $expected ||= {};
544 1   50     6 $test_description ||= "Expected queries for tables";
545 1         3 local $Test::Builder::Level = $Test::Builder::Level + 1;
546              
547 1         4 my $failure_message = $self->check_table_operation_counts($expected);
548 1         3 my $unknown_warning = $self->unknown_warning;
549              
550 1         32 $self->clear_queries();
551 1         20 $self->clear_table_operation_count();
552              
553 1 50       7 if($failure_message) {
554 0         0 fail($test_description);
555 0         0 diag("\n$failure_message");
556 0 0       0 $unknown_warning and note($unknown_warning);
557 0         0 return 0;
558             }
559              
560 1         18 pass($test_description);
561 1 50       538 $unknown_warning and note($unknown_warning);
562 1         3 return 1;
563             }
564              
565             sub check_table_operation_counts {
566 6     6 0 13752 my $self = shift;
567 6         12 my ($expected_table_count) = @_;
568              
569 6         165 my $table_operation_count = $self->table_operation_count();
570              
571             # Check actual events against test spec
572 6   50     39 my $expected_all_operation = $expected_table_count->{_all_} || {};
573 6         10 my $table_test_result = {};
574 6         11 for my $table (sort keys %{$table_operation_count}) {
  6         28  
575 13         22 my $operation_count = $table_operation_count->{$table};
576              
577 13         39 for my $operation (sort keys %$operation_count) {
578 17         25 my $actual_count = $operation_count->{$operation};
579 17         20 my $expected_outcome = do {
580 17 100       37 if ( exists $expected_table_count->{$table}->{$operation} ) {
    50          
581 14         20 $expected_table_count->{$table}->{$operation};
582             }
583             elsif (exists $expected_all_operation->{$operation}) {
584 0         0 $expected_all_operation->{$operation};
585             }
586 3         4 else { 0 }
587             };
588 17 50       37 defined($expected_outcome) or next;
589              
590 17         38 my $test_result = $self->test_count(
591             $table,
592             $operation,
593             $expected_outcome,
594             $actual_count,
595             );
596 17 100       52 $test_result and push(@{ $table_test_result->{$table} }, $test_result);
  3         12  
597             }
598             }
599              
600             # Check test spec against actual events to catch
601             ###JPL: extend this to validate test operations
602 6         26 my $operation_to_test = {
603             select => 1,
604             insert => 1,
605             update => 1,
606             delete => 1,
607             };
608 6         26 for my $table (sort keys %$expected_table_count) {
609 14         20 my $expected_operation_count = $expected_table_count->{$table};
610 14         29 for my $operation (sort keys %$expected_operation_count) {
611 18 100       33 next if ! $operation_to_test->{$operation};
612             # Already tested?
613 17 100       38 next if exists $table_operation_count->{$table}->{$operation};
614              
615 3         5 my $expected_outcome = $expected_operation_count->{$operation};
616 3 100       21 defined $expected_outcome or next; # undef = ignore
617              
618 2   50     9 my $actual_count = $table_operation_count->{$table}->{$operation} || 0;
619 2         5 my $test_result = $self->test_count(
620             $table,
621             $operation,
622             $expected_outcome,
623             $actual_count,
624             );
625 2 50       6 $test_result and push(@{ $table_test_result->{$table} }, $test_result);
  2         8  
626             }
627             }
628              
629 6 100       15 if(scalar keys %$table_test_result) {
630 4         7 my $message = "";
631 4         6 for my $table (sort keys %{$table_test_result}) {
  4         7  
632 5         13 $message .= "* Table: $table\n";
633 5         6 $message .= join("\n", @{$table_test_result->{$table}});
  5         11  
634 5         11 $message .= "\nActually executed SQL queries on table '$table':\n";
635 5         10 $message .= $self->sql_queries_for_table(
636             $table,
637             $expected_table_count,
638             ) . "\n\n";
639             }
640 4         21 return $message;
641             }
642 2         11 return "";
643             }
644              
645             sub unknown_warning {
646 2     2 0 618 my $self = shift;
647              
648 2 100       6 my @unknown_queries = $self->unknown_queries() or return "";
649              
650             return "\n\nWarning: unknown queries:\n" . join(
651             "\n",
652 1         3 map { $_->display_sql } @unknown_queries,
  1         3  
653             ) . "\n";
654             }
655              
656             sub unknown_queries {
657 2     2 0 4 my $self = shift;
658 2         4 return grep { ! $_->operation } @{$self->queries};
  9         26  
  2         45  
659             }
660              
661             sub sql_queries_for_table {
662 5     5 0 8 my $self = shift;
663 5         9 my ($table, $expected_table_count) = @_;
664              
665 5   100     20 my $stack_trace = $expected_table_count->{$table}->{stack_trace} || 0;
666              
667             return join(
668             "\n",
669             map {
670 9         19 my $out = $_->display_sql;
671 9 100       21 $stack_trace and $out .= "\n" . $_->display_stack_trace;
672 9         27 $out;
673             }
674 45   100     170 grep { lc($_->table // "") eq lc($table // "") }
      50        
675 5         7 @{$self->queries},
  5         93  
676             );
677             }
678              
679             sub test_count {
680 19     19 0 28 my $self = shift;
681 19         36 my ($table, $operation, $expected_outcome, $actual_count) = @_;
682              
683 19         28 my $expected_count;
684             my $operator;
685 19 100       71 if($expected_outcome =~ /^ \s* (\d+) /x) {
    50          
686 18         26 $operator = "==";
687 18         33 $expected_count = $1;
688             }
689             elsif($expected_outcome =~ /^ \s* (==|!=|>|>=|<|<=) \s* (\d+) /x) {
690 1         3 $operator = $1;
691 1         3 $expected_count = $2;
692             }
693             else {
694 0         0 croak("expect_queries: invalid comparison ($expected_outcome)\n");
695             }
696              
697             # actual, expected
698 19         36 my $comparison_perl = 'sub { $_[0] ' . $operator . ' $_[1] }';
699 19         1340 my $comparison = eval $comparison_perl; ## no critic
700 19 100       288 $comparison->($actual_count, $expected_count) and return "";
701              
702 5         46 return "Expected '$expected_outcome' ${operation}s for table '$table', got '$actual_count'";
703             }
704              
705             1;
706              
707              
708              
709             __END__
710              
711              
712             =head1 DEVELOPMENT
713              
714             =head2 Author
715              
716             Johan Lindstrom, C<< <johanl [AT] cpan.org> >>
717              
718              
719             =head2 Contributors
720              
721             Many thanks to:
722              
723             =over 4
724              
725             =item *
726              
727             Syohei YOSHIDA (syohex)
728              
729             =back
730              
731              
732             =head2 Source code
733              
734             L<https://github.com/jplindstrom/p5-Test-DBIC-ExpectedQueries>
735              
736              
737             =head2 Bug reports
738              
739             Please report any bugs or feature requests on GitHub:
740              
741             L<https://github.com/jplindstrom/p5-Test-DBIC-ExpectedQueries/issues>.
742              
743              
744             =head2 Caveats
745              
746             SQL queries are identified using quick-n-dirty regexes, to that might
747             be a bit brittle (and yet database agnostic, so there's that). Please
748             report cases with example SQL.
749              
750             If you have an anonymous subquery, that query might appear as a table
751             called "SELECT". If you find anything like this, or similar strange
752             results, please raise an issue on GitHub and provide the SQL text.
753              
754              
755              
756             =head1 COPYRIGHT & LICENSE
757              
758             Copyright 2015- Johan Lindstrom, All Rights Reserved.
759              
760             This program is free software; you can redistribute it and/or modify it
761             under the same terms as Perl itself.
762              
763             =cut