rspec-activerecord-expectations 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +39 -10
- data/lib/rspec/activerecord/expectations/collector.rb +16 -1
- data/lib/rspec/activerecord/expectations/matchers/load_matcher.rb +29 -0
- data/lib/rspec/activerecord/expectations/matchers/{query_count.rb → query_count_matcher.rb} +26 -12
- data/lib/rspec/activerecord/expectations/query_inspector.rb +10 -1
- data/lib/rspec/activerecord/expectations.rb +6 -1
- data/rspec-activerecord-expectations.gemspec +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 92f14ca413e9e716ea9b3046abc13d3929893185cfef8cdeaf14b7bebc93755c
|
4
|
+
data.tar.gz: 93bfa962b7954844560961d66d080fa2c45a3188b8c9b92420d1852e36ce718b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb81903bbb00357ab9613a59fb2684bfaad7671fbca955f9876d503f5b179f8ead6d36f248ea24909f30db3ddb6f4fc307c509a181d2fe85d33d1e04006856da
|
7
|
+
data.tar.gz: f813c1e8d6e320f6810cc33620fe702c379f47124b6add4bc1bdabde81c4e12c60a6aac5711caf8c23672f838d6185106ffb3695770a7c4002d04404c87929cb
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [1.3.0] - 2020-12-31
|
4
|
+
- Add `repeatedly_load` matcher
|
5
|
+
- Add query type matchers for `load_queries`, `schema_queries`, `transaction_queries`, `destroy_queries`
|
6
|
+
- Allow singular version of all query types (e.g. `transaction_queries` vs `transaction_query`)
|
7
|
+
- Fix failure message for `execute.exactly` matcher
|
8
|
+
|
3
9
|
## [1.2.0] - 2020-12-31
|
4
10
|
- Add `query` as a synonym for `queries`
|
5
11
|
- Ignore schema and transaction queries in query count
|
data/README.md
CHANGED
@@ -91,6 +91,30 @@ That's it! Refactor your report to be more efficient and then leave the test in
|
|
91
91
|
place to make sure that future developers don't accidentally cause a
|
92
92
|
performance regression.
|
93
93
|
|
94
|
+
## Preventing Repeated Load (N+1) Queries
|
95
|
+
|
96
|
+
Reloading, whether by e.g. `Album.find` or `album.tracks` are both antipatterns
|
97
|
+
within your code. They will load from the database for every iteration in a
|
98
|
+
loop, unless you load records outside the loop, cache responses, or use an
|
99
|
+
eager loading mechanism like `includes`. These sorts of queries are often
|
100
|
+
referred to as N+1 queries.
|
101
|
+
|
102
|
+
This sort of query can be prevented using the `repeatedly_load` expectation.
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
expect {}.not_to repeatedly_load('SomeActiveRecordClass')
|
106
|
+
```
|
107
|
+
|
108
|
+
This matcher will track ActiveRecord's built in load methods to prevent those
|
109
|
+
N+1 situations. Using eager loading (e.g. `Track.all.includes(:album)`) will
|
110
|
+
allow these expectations to pass as expected!
|
111
|
+
|
112
|
+
**Note:** At the moment, this expectation will fail if you use a mechanism
|
113
|
+
that loads records in batches, such as with `find_in_batches`. This will cause
|
114
|
+
records to be "repeatedly loaded", but this is actually expected behavior in
|
115
|
+
this case. If your tests load a relatively small number of records (which is
|
116
|
+
probable), your tests won't fail. But it is possible.
|
117
|
+
|
94
118
|
## Counting Queries
|
95
119
|
|
96
120
|
### Types of Comparisons
|
@@ -123,12 +147,22 @@ You can of course make assertions for the total number of queries executed, but
|
|
123
147
|
sometimes it's more valuable to assert particular _types_ of queries, such as
|
124
148
|
inserts or find statements. Matchers are supported for this as well!
|
125
149
|
|
126
|
-
**Note
|
150
|
+
**Note:** Transaction (for example, `ROLLBACK`) queries are not counted in any of these
|
127
151
|
categories, nor are queries that load the DB schema.
|
128
152
|
|
153
|
+
**Note:** Destroy and delete queries are both condensed into the matcher for
|
154
|
+
`destroy_queries`.
|
155
|
+
|
129
156
|
```ruby
|
130
157
|
expect {}.to execute.exactly(20).queries
|
158
|
+
|
131
159
|
expect {}.to execute.exactly(20).insert_queries
|
160
|
+
expect {}.to execute.exactly(20).load_queries
|
161
|
+
expect {}.to execute.exactly(20).destroy_queries
|
162
|
+
expect {}.to execute.exactly(20).exists_queries
|
163
|
+
|
164
|
+
expect {}.to execute.exactly(20).schema_queries
|
165
|
+
expect {}.to execute.exactly(20).transaction_queries
|
132
166
|
```
|
133
167
|
|
134
168
|
## Future Planned Functionality
|
@@ -136,12 +170,10 @@ expect {}.to execute.exactly(20).insert_queries
|
|
136
170
|
This gem still has lots of future functionality. See below.
|
137
171
|
|
138
172
|
```ruby
|
139
|
-
expect {}.to execute.at_least(2).activerecord_queries
|
140
|
-
expect {}.to execute.at_least(2).delete_queries
|
141
|
-
expect {}.to execute.at_least(2).load_queries
|
142
|
-
expect {}.to execute.at_least(2).schema_queries
|
143
|
-
expect {}.to execute.at_least(2).exists_queries
|
144
173
|
expect {}.to execute.at_least(2).queries_of_type("Audited::Audit Load")
|
174
|
+
expect {}.to execute.at_least(2).load_queries("Audited::Audit")
|
175
|
+
|
176
|
+
expect {}.to execute.at_least(2).activerecord_queries
|
145
177
|
expect {}.to execute.at_least(2).hand_rolled_queries
|
146
178
|
|
147
179
|
expect {}.not_to rollback_transaction.exactly(5).times
|
@@ -152,13 +184,10 @@ expect {}.to create.exactly(5).of_type(User)
|
|
152
184
|
expect {}.to insert.exactly(5).subscription_changes
|
153
185
|
expect {}.to update.exactly(2).of_any_type
|
154
186
|
expect {}.to delete.exactly(2).of_any_type
|
155
|
-
|
156
|
-
expect {}.not_to repeatedly_load(Audited::Audit)
|
157
187
|
```
|
158
188
|
|
159
|
-
- differentiate AR queries from generic ones? arbitrary execution somehow?
|
160
|
-
- warn about warmup
|
161
189
|
- warn if we smite any built in methods (or methods from other libs)
|
190
|
+
- support Rails 6 bulk insert (still one query)
|
162
191
|
|
163
192
|
## Development
|
164
193
|
|
@@ -2,11 +2,16 @@ module RSpec::ActiveRecord::Expectations
|
|
2
2
|
class Collector
|
3
3
|
def initialize
|
4
4
|
@inspector = QueryInspector.new
|
5
|
+
@by_name = {}
|
5
6
|
@counts = QueryInspector.valid_query_types.each_with_object({}) do |query_type, hash|
|
6
7
|
hash[query_type] = 0
|
7
8
|
end
|
8
9
|
|
9
|
-
ActiveSupport::Notifications.subscribe("sql.active_record", method(:record_query))
|
10
|
+
@subscription = ActiveSupport::Notifications.subscribe("sql.active_record", method(:record_query))
|
11
|
+
end
|
12
|
+
|
13
|
+
def finalize
|
14
|
+
ActiveSupport::Notifications.unsubscribe(@subscription)
|
10
15
|
end
|
11
16
|
|
12
17
|
def queries_of_type(type)
|
@@ -17,11 +22,21 @@ module RSpec::ActiveRecord::Expectations
|
|
17
22
|
@counts.include? type
|
18
23
|
end
|
19
24
|
|
25
|
+
def calls_by_name(name)
|
26
|
+
@by_name.fetch(name, 0)
|
27
|
+
end
|
28
|
+
|
20
29
|
def record_query(*_unused, data)
|
21
30
|
categories = @inspector.categorize(data)
|
31
|
+
|
22
32
|
categories.each do |category|
|
23
33
|
@counts[category] += 1
|
34
|
+
rescue NoMethodError
|
35
|
+
raise "tried to add to to #{category} but it doesn't exist"
|
24
36
|
end
|
37
|
+
|
38
|
+
@by_name[data[:name]] ||= 0
|
39
|
+
@by_name[data[:name]] += 1
|
25
40
|
end
|
26
41
|
end
|
27
42
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module RSpec::ActiveRecord::Expectations
|
2
|
+
module Matchers
|
3
|
+
class LoadMatcher
|
4
|
+
def initialize(klass)
|
5
|
+
@collector = Collector.new
|
6
|
+
@klass = klass.to_s
|
7
|
+
end
|
8
|
+
|
9
|
+
def supports_block_expectations?
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches?(block)
|
14
|
+
block.call
|
15
|
+
|
16
|
+
@count = @collector.calls_by_name("#{@klass} Load")
|
17
|
+
@count > 1
|
18
|
+
end
|
19
|
+
|
20
|
+
def failure_message
|
21
|
+
"expected block to repeatedly load #{@klass}, but it was loaded #{@count} times"
|
22
|
+
end
|
23
|
+
|
24
|
+
def failure_message_when_negated
|
25
|
+
"expected block not to repeatedly load #{@klass}, but it was loaded #{@count} times"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -19,9 +19,11 @@ module RSpec::ActiveRecord::Expectations
|
|
19
19
|
raise NoComparisonError unless @match_method
|
20
20
|
raise NoQueryTypeError unless @collector.valid_type?(@query_type)
|
21
21
|
|
22
|
-
|
22
|
+
block.call
|
23
|
+
result = @match_method.call
|
24
|
+
@collector.finalize
|
23
25
|
|
24
|
-
|
26
|
+
result
|
25
27
|
end
|
26
28
|
|
27
29
|
# COMPARISON TYPES
|
@@ -67,6 +69,14 @@ module RSpec::ActiveRecord::Expectations
|
|
67
69
|
@query_type = type
|
68
70
|
self
|
69
71
|
end
|
72
|
+
|
73
|
+
singularized_type = type.to_s.gsub('queries', 'query')
|
74
|
+
if singularized_type != type.to_s
|
75
|
+
define_method singularized_type do
|
76
|
+
@query_type = type
|
77
|
+
self
|
78
|
+
end
|
79
|
+
end
|
70
80
|
end
|
71
81
|
|
72
82
|
# TODO singularize everything
|
@@ -74,13 +84,17 @@ module RSpec::ActiveRecord::Expectations
|
|
74
84
|
|
75
85
|
private
|
76
86
|
|
87
|
+
def humanized_query_type
|
88
|
+
@query_type.to_s.gsub("_", " ")
|
89
|
+
end
|
90
|
+
|
77
91
|
# MATCHERS
|
78
92
|
|
79
93
|
def compare_less_than
|
80
94
|
count = @collector.queries_of_type(@query_type)
|
81
95
|
|
82
|
-
@failure_message = "expected block to execute fewer than #{@comparison}
|
83
|
-
@failure_message_when_negated = "expected block not to execute fewer than #{@comparison}
|
96
|
+
@failure_message = "expected block to execute fewer than #{@comparison} #{humanized_query_type}, but it executed #{count}"
|
97
|
+
@failure_message_when_negated = "expected block not to execute fewer than #{@comparison} #{humanized_query_type}, but it executed #{count}"
|
84
98
|
|
85
99
|
count < @comparison
|
86
100
|
end
|
@@ -89,8 +103,8 @@ module RSpec::ActiveRecord::Expectations
|
|
89
103
|
count = @collector.queries_of_type(@query_type)
|
90
104
|
|
91
105
|
# boy this negated operator is confusing. don't use that plz.
|
92
|
-
@failure_message = "expected block to execute at most #{@comparison}
|
93
|
-
@failure_message_when_negated = "expected block not to execute any less than #{@comparison}
|
106
|
+
@failure_message = "expected block to execute at most #{@comparison} #{humanized_query_type}, but it executed #{count}"
|
107
|
+
@failure_message_when_negated = "expected block not to execute any less than #{@comparison} #{humanized_query_type}, but it executed #{count}"
|
94
108
|
|
95
109
|
count <= @comparison
|
96
110
|
end
|
@@ -98,8 +112,8 @@ module RSpec::ActiveRecord::Expectations
|
|
98
112
|
def compare_greater_than
|
99
113
|
count = @collector.queries_of_type(@query_type)
|
100
114
|
|
101
|
-
@failure_message = "expected block to execute greater than #{@comparison}
|
102
|
-
@failure_message_when_negated = "expected block not to execute greater than #{@comparison}
|
115
|
+
@failure_message = "expected block to execute greater than #{@comparison} #{humanized_query_type}, but it executed #{count}"
|
116
|
+
@failure_message_when_negated = "expected block not to execute greater than #{@comparison} #{humanized_query_type}, but it executed #{count}"
|
103
117
|
|
104
118
|
count > @comparison
|
105
119
|
end
|
@@ -108,8 +122,8 @@ module RSpec::ActiveRecord::Expectations
|
|
108
122
|
count = @collector.queries_of_type(@query_type)
|
109
123
|
|
110
124
|
# see above, negating this matcher is so confusing.
|
111
|
-
@failure_message = "expected block to execute at least #{@comparison}
|
112
|
-
@failure_message_when_negated = "expected block not to execute any more than #{@comparison}
|
125
|
+
@failure_message = "expected block to execute at least #{@comparison} #{humanized_query_type}, but it executed #{count}"
|
126
|
+
@failure_message_when_negated = "expected block not to execute any more than #{@comparison} #{humanized_query_type}, but it executed #{count}"
|
113
127
|
|
114
128
|
count >= @comparison
|
115
129
|
end
|
@@ -117,8 +131,8 @@ module RSpec::ActiveRecord::Expectations
|
|
117
131
|
def compare_exactly
|
118
132
|
count = @collector.queries_of_type(@query_type)
|
119
133
|
|
120
|
-
@failure_message = "expected block to execute
|
121
|
-
@failure_message_when_negated = "expected block not to execute #{@comparison}
|
134
|
+
@failure_message = "expected block to execute exactly #{@comparison} #{humanized_query_type}, but it executed #{count}"
|
135
|
+
@failure_message_when_negated = "expected block not to execute exactly #{@comparison} #{humanized_query_type}, but it executed #{count}"
|
122
136
|
|
123
137
|
count == @comparison
|
124
138
|
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
module RSpec::ActiveRecord::Expectations
|
2
2
|
class QueryInspector
|
3
3
|
def self.valid_query_types
|
4
|
-
[:queries, :schema_queries, :transaction_queries, :insert_queries
|
4
|
+
[:queries, :schema_queries, :transaction_queries, :insert_queries,
|
5
|
+
:load_queries, :destroy_queries, :exists_queries]
|
5
6
|
end
|
6
7
|
|
7
8
|
def categorize(query)
|
@@ -11,6 +12,14 @@ module RSpec::ActiveRecord::Expectations
|
|
11
12
|
[:transaction_queries]
|
12
13
|
elsif query[:name] =~ /Create$/
|
13
14
|
[:queries, :insert_queries]
|
15
|
+
elsif query[:name] =~ /Load$/
|
16
|
+
[:queries, :load_queries]
|
17
|
+
elsif query[:name] =~ /Destroy$/
|
18
|
+
[:queries, :destroy_queries]
|
19
|
+
elsif query[:name] =~ /Delete All$/
|
20
|
+
[:queries, :destroy_queries]
|
21
|
+
elsif query[:name] =~ /Exists\?$/
|
22
|
+
[:queries, :exists_queries]
|
14
23
|
else
|
15
24
|
[:queries]
|
16
25
|
end
|
@@ -4,6 +4,10 @@ module RSpec
|
|
4
4
|
def execute
|
5
5
|
Matchers::QueryCountMatcher.new
|
6
6
|
end
|
7
|
+
|
8
|
+
def repeatedly_load(klass)
|
9
|
+
Matchers::LoadMatcher.new(klass)
|
10
|
+
end
|
7
11
|
end
|
8
12
|
end
|
9
13
|
end
|
@@ -11,4 +15,5 @@ end
|
|
11
15
|
require_relative 'expectations/errors'
|
12
16
|
require_relative 'expectations/query_inspector'
|
13
17
|
require_relative 'expectations/collector'
|
14
|
-
require_relative 'expectations/matchers/
|
18
|
+
require_relative 'expectations/matchers/query_count_matcher'
|
19
|
+
require_relative 'expectations/matchers/load_matcher'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-activerecord-expectations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joseph Mastey
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -102,7 +102,8 @@ files:
|
|
102
102
|
- lib/rspec/activerecord/expectations.rb
|
103
103
|
- lib/rspec/activerecord/expectations/collector.rb
|
104
104
|
- lib/rspec/activerecord/expectations/errors.rb
|
105
|
-
- lib/rspec/activerecord/expectations/matchers/
|
105
|
+
- lib/rspec/activerecord/expectations/matchers/load_matcher.rb
|
106
|
+
- lib/rspec/activerecord/expectations/matchers/query_count_matcher.rb
|
106
107
|
- lib/rspec/activerecord/expectations/query_inspector.rb
|
107
108
|
- rspec-activerecord-expectations.gemspec
|
108
109
|
homepage: https://github.com/jmmastey/rspec-activerecord-expectations
|