File Coverage

lib/Test/DBIC/ExpectedQueries.pm
Criterion Covered Total %
statement 143 162 88.2
branch 29 38 76.3
condition 9 14 64.2
subroutine 20 24 83.3
pod 3 8 37.5
total 204 246 82.9


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