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