File Coverage

blib/lib/SimpleMock/Model/DBI.pm
Criterion Covered Total %
statement 53 53 100.0
branch 11 12 91.6
condition 12 13 92.3
subroutine 10 10 100.0
pod 0 1 0.0
total 86 89 96.6


line stmt bran cond sub pod time code
1             package SimpleMock::Model::DBI;
2 4     4   54 use strict;
  4         4  
  4         133  
3 4     4   17 use warnings;
  4         16  
  4         193  
4 4     4   14 use DBI;
  4         4  
  4         215  
5 4     4   12 use Storable qw(dclone);
  4         25  
  4         242  
6 4     4   19 use Data::Dumper;
  4         4  
  4         227  
7              
8 4         2392 use SimpleMock::Util qw(
9             generate_args_sha
10 4     4   15 );
  4         10  
11              
12             our $VERSION = '0.01';
13              
14             our $drh = DBI->install_driver('SimpleMock');
15              
16             our @valid_global_meta_keys = (
17             # 0|1 allow queries that are not mocked to run with a default empty result set
18             'allow_unmocked_queries',
19              
20             # 0|1 if true, then $dbh->connect returns undef (use for error checking tests)
21             'connect_fail',
22              
23             # 0|1 if true, then $dbh->prepare fails with invalid SQL error
24             'prepare_fail',
25              
26             # 0|1 if true, then $sth->execute returns undef (use for error checking tests)
27             'execute_fail',
28             );
29              
30             our %valid_global_meta_keys_lookup;
31             undef @valid_global_meta_keys_lookup{ @valid_global_meta_keys };
32              
33             # lowercase and remove double spaces - I know some DBs are case sensitive, but
34             # this can simplify catching typos in tests
35             sub _normalize_sql {
36 21     21   55 my ($sql) = @_;
37 21         33 $sql = lc($sql);
38 21         211 $sql =~ s/ +/ /g;
39 21         49 return $sql;
40             }
41              
42             sub validate_mocks {
43 10     10 0 17 my $mocks_data = shift;
44              
45 10         15 my $new_mocks = {};
46              
47 10   100     27 my $meta = $mocks_data->{META} || {};
48             # only one option initially, but add more as needed
49 10         19 META: foreach my $key (keys %$meta) {
50 5 100       18 die "unknown meta key: $key" unless exists $valid_global_meta_keys_lookup{$key};
51 4         10 $new_mocks->{DBI}->{_meta}->{$key} = $meta->{$key};
52             }
53              
54 9   100     25 my $queries = $mocks_data->{QUERIES} || [];
55              
56 9         13 QUERY: foreach my $query (@$queries) {
57 6         11 my $normalized_sql = _normalize_sql($query->{sql});
58 6   100     22 my $cols = $query->{cols} || [];
59 6 100       20 RESULT: foreach my $result (@{$query->{results} || []}) {
  6         17  
60 8   50     21 my $data = $result->{data} || [[]];
61 8         25 my $sha = generate_args_sha($result->{args});
62             my $mock = {
63             data => $data,
64             cols => $cols,
65 8   100     252 args => $result->{args} || [],
66             };
67 8         450 $new_mocks->{DBI}->{$normalized_sql}->{$sha} = dclone($mock);
68             }
69             }
70 9         23 return $new_mocks;
71             }
72              
73             sub _get_dbi_meta {
74 44     44   50 my $key = shift;
75 44         92 for my $layer (reverse @SimpleMock::MOCK_STACK) {
76             return $layer->{DBI}{_meta}{$key}
77 49 100       123 if exists $layer->{DBI}{_meta}{$key};
78             }
79 40         82 return undef;
80             }
81              
82             sub _get_mock_for {
83 15     15   21 my ($sql, $args) = @_;
84 15         20 my $normalized = _normalize_sql($sql);
85 15         42 my $sha = generate_args_sha($args);
86              
87 15         766 for my $layer (reverse @SimpleMock::MOCK_STACK) {
88 16 50       35 my $dbi = $layer->{DBI} or next;
89 16   100     53 my $mock = $dbi->{$normalized}{$sha} || $dbi->{$normalized}{'_default'};
90 16 100       540 return dclone($mock) if $mock;
91             }
92              
93             # allow_unmocked_queries can be set in any layer
94 2         4 for my $layer (reverse @SimpleMock::MOCK_STACK) {
95 2 100       51 return dclone({ data => [[]] }) if $layer->{DBI}{_meta}{allow_unmocked_queries};
96             }
97              
98 1         3 die "No mock data found for '$normalized' with args: " . Dumper($args);
99             }
100              
101             1;
102              
103             =head1 NAME
104              
105             SimpleMock::Model::DBI - A mock model for DBI queries
106              
107             =head1 DESCRIPTION
108              
109             This module provides a mock model for DBI queries, allowing you to register
110             mock queries and their results. It normalizes queries and handles argument-based mocking.
111              
112             Metadata can be set to control behavior such as allowing unmocked queries, or to force
113             failure on certain operations like `prepare`, `execute` or `connect`.
114              
115             =head1 USAGE
116              
117             You probably won't want to use this module directly, but rather use the SimpleMock
118             module in your tests instead:
119              
120             use SimpleMock qw(register_mocks);
121              
122             register_mocks(
123             DBI => {
124             # all meta values default to false if not explicitly set
125             META => {
126             # 0|1 allow queries that are not mocked to run with a default empty result set
127             'allow_unmocked_queries' => 1,
128              
129             # 0|1 if true, then $dbh->connect returns undef (use for error checking tests)
130             'connect_fail' => 0,
131              
132             # 0|1 if true, then $dbh->prepare fails with invalid SQL error
133             'prepare_fail' => 0,
134            
135             # 0|1 if true, then $sth->execute returns undef (use for error checking tests)
136             'execute_fail' => 0,
137             },
138             # QUERIES is an array of individual sql statements and what to return when executed
139             # with specific args
140             QUERIES => [
141             {
142             sql => 'SELECT name, email FROM users WHERE id = ?',
143             results => [
144              
145             # specific result data for arg sent
146             { args => [1],
147             data => [ ['Alice', 'alice@example.com'] ] },
148              
149             # specific result data for arg sent
150             { args => [2],
151             data => [ ['Bob', 'bob@example.com'] ] },
152              
153             # result data for all other args
154             { data => [ ['Default', 'default@example.com'] ] },
155              
156             ],
157             },
158             ],
159             },
160             );
161              
162             For each query, specify the SQL statement. Then, in the results array, provide the
163             placeholder args and data to return for each, and an optional default result that only
164             has a data element to use as a default for query executions where there is no args match
165              
166             =cut