rspec-activerecord-expectations 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea0eb161965acaf677cec0de83466eece5379423903fc0a04b181cabe61a8121
4
- data.tar.gz: 4df14e9211baf4ac6f3935a68b910191480f6f6af622f36c7d7e51a90374d222
3
+ metadata.gz: 92f14ca413e9e716ea9b3046abc13d3929893185cfef8cdeaf14b7bebc93755c
4
+ data.tar.gz: 93bfa962b7954844560961d66d080fa2c45a3188b8c9b92420d1852e36ce718b
5
5
  SHA512:
6
- metadata.gz: 75bd827d5d0fc6566ba54f48cd432fab6cad301f2b5eb781383ea227ec74e2699733026fc22086899ac8bf725280e9cb1a5d7ca42545074cee1eb932142f9d93
7
- data.tar.gz: ad9f387210fb8d30c06b5177a51b934a415d36805d94729d8117408200b241ee3f9408884bfbe63ab2bc93b28118f69a13dbe1aa05e3a2eb80bf04705a6b3de4
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**: Transaction (for example, `ROLLBACK`) queries are not counted in any of these
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
- result = block.call
22
+ block.call
23
+ result = @match_method.call
24
+ @collector.finalize
23
25
 
24
- !!@match_method.call
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} queries, but it executed #{count}"
83
- @failure_message_when_negated = "expected block not to execute fewer than #{@comparison} queries, but it executed #{count}"
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} queries, but it executed #{count}"
93
- @failure_message_when_negated = "expected block not to execute any less than #{@comparison} queries, but it executed #{count}"
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} queries, but it executed #{count}"
102
- @failure_message_when_negated = "expected block not to execute greater than #{@comparison} queries, but it executed #{count}"
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} queries, but it executed #{count}"
112
- @failure_message_when_negated = "expected block not to execute any more than #{@comparison} queries, but it executed #{count}"
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 at #{@comparison} queries, but it executed #{count}"
121
- @failure_message_when_negated = "expected block not to execute #{@comparison} queries, but it executed #{count}"
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/query_count'
18
+ require_relative 'expectations/matchers/query_count_matcher'
19
+ require_relative 'expectations/matchers/load_matcher'
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "rspec-activerecord-expectations"
3
- spec.version = '1.2.0'
3
+ spec.version = '1.3.0'
4
4
  spec.authors = ["Joseph Mastey"]
5
5
  spec.email = ["hello@joemastey.com"]
6
6
 
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.2.0
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: 2021-12-31 00:00:00.000000000 Z
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/query_count.rb
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