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 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