n_plus_one_control 0.6.1 → 0.7.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: d9eb89619e16cb318f7c758c355c9578b1bbe10b446aa86a8e6a05402e5a3634
4
- data.tar.gz: c441fc080ac62c150e21877868e8862175b22521d8cb12d3ce240d5153b2d7d7
3
+ metadata.gz: 0fed1df4b3bee8844b57b4d4201802361b5871d93db3fdb0132a4073fd56fc3a
4
+ data.tar.gz: 56649719e9b94ed7eb05ccf3c6236399a6a2a9394ce37574a8e58f079b1ff3b4
5
5
  SHA512:
6
- metadata.gz: 5e5e62dcd9958539060f05f25758a24cfedaa17d3b78a0439c0dc2fbc649c8ba9467371c56aecc78f722c0426daf70db8dea94e5ab8ea86bdc2cfec93c83669a
7
- data.tar.gz: 26cd504b1ca3ed20ce7b25f12a80ab3d8470a1ccefe66bc432af51005bbfd3bc30e3a770a04d7b4f528eac393581f56c2fa044034399c6741f7316b5444b22d2
6
+ metadata.gz: cae71e3487fb5210bd6dd76ae0cf7ee1bb310748a3e3a9cd95d893dc2eb99c6fa90c8482366897a7a2e6c8ed479780d337ef2e1284c266fef00e7b67f5f42dad
7
+ data.tar.gz: 270f6a75e499ffecd886d98739ccaa5f15ff60a7aebc1ebecc10cc0b2cfdd831137e482897d6ed217f0e60306e2b0350ef22d781a0475ad1a7dfda7563d6fc35
data/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.7.0 (2023-02-17)
6
+
7
+ - Added ability to specify the exact number of expected queries when using constant matchers. ([@akostadinov][], [@palkan][])
8
+
9
+ For RSpec, you can add the `.exactly` modifier:
10
+
11
+ ```ruby
12
+ expect { get :index }.to perform_constant_number_of_queries.exactly(1)
13
+ ```
14
+
15
+ For Minitest, you can provide the expected number of queries as the first argument:
16
+
17
+ ```ruby
18
+ assert_perform_constant_number_of_queries(0, **options) do
19
+ get :index
20
+ end
21
+ ```
22
+
23
+ - **Require Ruby 2.7+**.
24
+
25
+ ## 0.6.2 (2021-10-26)
26
+
27
+ - Fix .ignore setting (.ignore setting was ignored by the Collector ;-))
28
+ - Fix rspec matchers to allow expectations inside execution block
29
+
5
30
  ## 0.6.1 (2021-03-05)
6
31
 
7
32
  - Ruby 3.0 compatibility. ([@palkan][])
@@ -39,3 +64,4 @@ Could be specified via `NPLUSONE_BACKTRACE` env var.
39
64
  [@palkan]: https://github.com/palkan
40
65
  [@caalberts]: https://github.com/caalberts
41
66
  [@andrewhampton]: https://github.com/andrewhampton
67
+ [@akostadinov]: https://github.com/akostadinov
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2017-2020 Vladimir Dementyev
3
+ Copyright (c) 2017-2023 Vladimir Dementyev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -17,6 +17,8 @@ NPlusOneControl works differently. It evaluates the code under consideration sev
17
17
 
18
18
  So, it's for _performance_ testing and not _feature_ testing.
19
19
 
20
+ > Read also ["Squash N+1 queries early with n_plus_one_control test matchers for Ruby and Rails"](https://evilmartians.com/chronicles/squash-n-plus-one-queries-early-with-n-plus-one-control-test-matchers-for-ruby-and-rails).
21
+
20
22
  ### Why not just use [`bullet`](https://github.com/flyerhzm/bullet)?
21
23
 
22
24
  Of course, it's possible to use Bullet in tests (see more [here](https://evilmartians.com/chronicles/fighting-the-hydra-of-n-plus-one-queries)), but it's not a _silver bullet_: there can be both false positives and true negatives.
@@ -72,12 +74,33 @@ end
72
74
 
73
75
  ```ruby
74
76
  # BAD – won't work!
75
-
76
77
  subject { get :index }
77
78
 
78
79
  specify do
79
80
  expect { subject }.to perform_constant_number_of_queries
80
81
  end
82
+
83
+ # GOOD
84
+ specify do
85
+ expect { get :index }.to perform_constant_number_of_queries
86
+ end
87
+
88
+ # BAD — the `page` record would be removed from the database
89
+ # but still present in RSpec (due to `let`'s memoization)
90
+ let(:page) { create(:page) }
91
+
92
+ populate { |n| create_list(:comment, n, page: page) }
93
+
94
+ specify do
95
+ expect { get :show, params: {id: page.id} }.to perform_constant_number_of_queries
96
+ end
97
+
98
+ # GOOD
99
+ # Ensure the record is created before `populate`
100
+ let!(:page) { create(:page) }
101
+
102
+ populate { |n| create_list(:comment, n, page: page) }
103
+ # ...
81
104
  ```
82
105
 
83
106
  Availables modifiers:
@@ -89,6 +112,9 @@ expect { get :index }.to perform_constant_number_of_queries.matching(/INSERT/)
89
112
 
90
113
  # You can also provide custom scale factors
91
114
  expect { get :index }.to perform_constant_number_of_queries.with_scale_factors(10, 100)
115
+
116
+ # You can specify the exact number of expected queries
117
+ expect { get :index }.to perform_constant_number_of_queries.exactly(1)
92
118
  ```
93
119
 
94
120
  #### Using scale factor in spec
@@ -111,6 +137,20 @@ context "N+1", :n_plus_one do
111
137
  end
112
138
  ```
113
139
 
140
+ ### Expectations in execution block
141
+
142
+ Both rspec matchers allows you to put additional expectations inside execution block to ensure that tested piece of code actually does what expected.
143
+
144
+ ```ruby
145
+ context "N+1", :n_plus_one do
146
+ specify do
147
+ expect do
148
+ expect(my_query).to eq(actuall_results)
149
+ end.to perform_constant_number_of_queries
150
+ end
151
+ end
152
+ ```
153
+
114
154
  #### Other available matchers
115
155
 
116
156
  `perform_linear_number_of_queries(slope: 1)` allows you to test that a query generates linear number of queries with the given slope.
@@ -152,7 +192,7 @@ You can also use `assert_perform_linear_number_of_queries` to test for linear qu
152
192
  ```ruby
153
193
  def test_no_n_plus_one_error
154
194
  populate = ->(n) { create_list(:post, n) }
155
-
195
+
156
196
  assert_perform_linear_number_of_queries(slope: 1, populate: populate) do
157
197
  Post.find_each { |p| p.user.name }
158
198
  end
@@ -177,6 +217,14 @@ assert_perform_constant_number_of_queries(
177
217
  end
178
218
  ```
179
219
 
220
+ For the constant matcher, you can also specify the expected number of queries as the first argument:
221
+
222
+ ```ruby
223
+ assert_perform_constant_number_of_queries(2, populate: populate) do
224
+ get :index
225
+ end
226
+ ```
227
+
180
228
  It's possible to specify a filter via `NPLUSONE_FILTER` env var, e.g.:
181
229
 
182
230
  ```ruby
@@ -256,7 +304,7 @@ end
256
304
  If your `warmup` and testing procs are identical, you can use:
257
305
 
258
306
  ```ruby
259
- expext { get :index }.to perform_constant_number_of_queries.with_warming_up # RSpec only
307
+ expect { get :index }.to perform_constant_number_of_queries.with_warming_up # RSpec only
260
308
  ```
261
309
 
262
310
  ### Configuration
@@ -288,7 +336,7 @@ NPlusOneControl.ignore = /^(BEGIN|COMMIT|SAVEPOINT|RELEASE)/
288
336
  # but can also track rom-rb events ('sql.rom') as well.
289
337
  NPlusOneControl.event = "sql.active_record"
290
338
 
291
- # configure transactional behavour for populate method
339
+ # configure transactional behaviour for populate method
292
340
  # in case of use multiple database connections
293
341
  NPlusOneControl::Executor.tap do |executor|
294
342
  connections = ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
@@ -312,7 +360,7 @@ NPlusOneControl.backtrace_cleaner = ->(locations_array) { do_some_filtering(loca
312
360
  NPlusOneControl.backtrace_length = 1
313
361
 
314
362
  # Sometime queries could be too large to provide any meaningful insight.
315
- # You can configure an output length limit for quries in verbose mode by setting the follwing option
363
+ # You can configure an output length limit for quries in verbose mode by setting the following option
316
364
  # NOTE: It could be specified via NPLUSONE_TRUNCATE env var
317
365
  NPlusOneControl.truncate_query_size = 100
318
366
  ```
@@ -21,6 +21,7 @@ module NPlusOneControl
21
21
 
22
22
  def callback(_name, _start, _finish, _message_id, values) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/LineLength
23
23
  return if %w[CACHE SCHEMA].include? values[:name]
24
+ return if values[:sql].match?(NPlusOneControl.ignore)
24
25
 
25
26
  return unless @pattern.nil? || (values[:sql] =~ @pattern)
26
27
 
@@ -6,6 +6,7 @@ module NPlusOneControl
6
6
  # Minitest assertions
7
7
  module MinitestHelper
8
8
  def assert_perform_constant_number_of_queries(
9
+ exact = nil,
9
10
  populate: nil,
10
11
  matching: nil,
11
12
  scale_factors: nil,
@@ -26,7 +27,11 @@ module NPlusOneControl
26
27
 
27
28
  counts = queries.map(&:last).map(&:size)
28
29
 
29
- assert counts.max == counts.min, NPlusOneControl.failure_message(:constant_queries, queries)
30
+ if exact
31
+ assert counts.all? { _1 == exact }, NPlusOneControl.failure_message(:number_of_queries, queries)
32
+ else
33
+ assert counts.max == counts.min, NPlusOneControl.failure_message(:constant_queries, queries)
34
+ end
30
35
  end
31
36
 
32
37
  def assert_perform_linear_number_of_queries(
@@ -59,7 +64,7 @@ module NPlusOneControl
59
64
  private
60
65
 
61
66
  def warming_up(warmup)
62
- (warmup || methods.include?(:warmup) ? method(:warmup) : nil)&.call
67
+ (warmup || (methods.include?(:warmup) ? method(:warmup) : nil))&.call
63
68
  end
64
69
 
65
70
  def population_method
@@ -6,7 +6,7 @@ module NPlusOneControl
6
6
  module DSL
7
7
  # Extends RSpec ExampleGroup with populate & warmup methods
8
8
  module ClassMethods
9
- # Setup warmup block, wich will run before matching
9
+ # Setup warmup block, which will run before matching
10
10
  # for example, if using cache, then later queries
11
11
  # will perform less DB queries than first
12
12
  def warmup(&block)
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/BlockLength
4
3
  ::RSpec::Matchers.define :perform_constant_number_of_queries do
5
4
  supports_block_expectations
6
5
 
@@ -12,11 +11,15 @@
12
11
  @pattern = pattern
13
12
  end
14
13
 
14
+ chain :exactly do |pattern|
15
+ @exactly = pattern
16
+ end
17
+
15
18
  chain :with_warming_up do
16
19
  @warmup = true
17
20
  end
18
21
 
19
- match do |actual, *_args|
22
+ match(notify_expectation_failures: true) do |actual, *_args|
20
23
  raise ArgumentError, "Block is required" unless actual.is_a? Proc
21
24
 
22
25
  raise "Missing tag :n_plus_one" unless
@@ -32,20 +35,23 @@
32
35
  @matcher_execution_context.executor = NPlusOneControl::Executor.new(
33
36
  population: populate,
34
37
  matching: pattern,
35
- scale_factors: @factors
38
+ scale_factors: @exactly ? [1] : @factors
36
39
  )
37
40
 
38
41
  @queries = @matcher_execution_context.executor.call(&actual)
39
42
 
40
43
  counts = @queries.map(&:last).map(&:size)
41
44
 
42
- counts.max == counts.min
45
+ if @exactly
46
+ counts.all? { _1 == @exactly }
47
+ else
48
+ counts.max == counts.min
49
+ end
43
50
  end
44
51
 
45
52
  match_when_negated do |_actual|
46
53
  raise "This matcher doesn't support negation"
47
54
  end
48
55
 
49
- failure_message { |_actual| NPlusOneControl.failure_message(:constant_queries, @queries) }
56
+ failure_message { |_actual| NPlusOneControl.failure_message(@exactly ? :number_of_queries : :constant_queries, @queries) }
50
57
  end
51
- # rubocop:enable Metrics/BlockLength
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/BlockLength
4
3
  ::RSpec::Matchers.define :perform_linear_number_of_queries do |slope: 1|
5
4
  supports_block_expectations
6
5
 
@@ -16,7 +15,7 @@
16
15
  @warmup = true
17
16
  end
18
17
 
19
- match do |actual, *_args|
18
+ match(notify_expectation_failures: true) do |actual, *_args|
20
19
  raise ArgumentError, "Block is required" unless actual.is_a? Proc
21
20
 
22
21
  raise "Missing tag :n_plus_one" unless
@@ -50,4 +49,3 @@
50
49
 
51
50
  failure_message { |_actual| NPlusOneControl.failure_message(:linear_queries, @queries) }
52
51
  end
53
- # rubocop:enable Metrics/BlockLength
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NPlusOneControl
4
- VERSION = "0.6.1"
4
+ VERSION = "0.7.0"
5
5
  end
@@ -25,7 +25,8 @@ module NPlusOneControl
25
25
 
26
26
  FAILURE_MESSAGES = {
27
27
  constant_queries: "Expected to make the same number of queries",
28
- linear_queries: "Expected to make linear number of queries"
28
+ linear_queries: "Expected to make linear number of queries",
29
+ number_of_queries: "Expected to make the specified number of queries"
29
30
  }
30
31
 
31
32
  def failure_message(type, queries) # rubocop:disable Metrics/MethodLength
@@ -59,9 +60,10 @@ module NPlusOneControl
59
60
  end
60
61
 
61
62
  before.keys.each do |k|
62
- next if before[k] == after[k]
63
+ next if before[k] == after&.fetch(k, nil)
63
64
 
64
- msg << "#{k}: #{before[k]} != #{after[k]}\n"
65
+ after_value = after ? " != #{after[k]}" : ""
66
+ msg << "#{k}: #{before[k]}#{after_value}\n"
65
67
  end
66
68
 
67
69
  msg
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: n_plus_one_control
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-05 00:00:00.000000000 Z
11
+ date: 2023-02-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -67,19 +67,19 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '5.9'
69
69
  - !ruby/object:Gem::Dependency
70
- name: factory_girl
70
+ name: factory_bot
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 4.8.0
75
+ version: '6.0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 4.8.0
82
+ version: '6.0'
83
83
  description: "\n RSpec and Minitest matchers to prevent N+1 queries problem.\n\n
84
84
  \ Evaluates code under consideration several times with different scale factors\n
85
85
  \ to make sure that the number of DB queries behaves as expected (i.e. O(1) instead
@@ -103,16 +103,16 @@ files:
103
103
  - lib/n_plus_one_control/rspec/matchers/perform_constant_number_of_queries.rb
104
104
  - lib/n_plus_one_control/rspec/matchers/perform_linear_number_of_queries.rb
105
105
  - lib/n_plus_one_control/version.rb
106
- homepage: http://github.com/palkan/n_plus_one_control
106
+ homepage: https://github.com/palkan/n_plus_one_control
107
107
  licenses:
108
108
  - MIT
109
109
  metadata:
110
- bug_tracker_uri: http://github.com/palkan/n_plus_one_control/issues
110
+ bug_tracker_uri: https://github.com/palkan/n_plus_one_control/issues
111
111
  changelog_uri: https://github.com/palkan/n_plus_one_control/blob/master/CHANGELOG.md
112
- documentation_uri: http://github.com/palkan/n_plus_one_control
113
- homepage_uri: http://github.com/palkan/n_plus_one_control
114
- source_code_uri: http://github.com/palkan/n_plus_one_control
115
- post_install_message:
112
+ documentation_uri: https://github.com/palkan/n_plus_one_control
113
+ homepage_uri: https://github.com/palkan/n_plus_one_control
114
+ source_code_uri: https://github.com/palkan/n_plus_one_control
115
+ post_install_message:
116
116
  rdoc_options: []
117
117
  require_paths:
118
118
  - lib
@@ -120,15 +120,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - ">="
122
122
  - !ruby/object:Gem::Version
123
- version: 2.5.0
123
+ version: 2.7.0
124
124
  required_rubygems_version: !ruby/object:Gem::Requirement
125
125
  requirements:
126
126
  - - ">="
127
127
  - !ruby/object:Gem::Version
128
128
  version: '0'
129
129
  requirements: []
130
- rubygems_version: 3.0.6
131
- signing_key:
130
+ rubygems_version: 3.4.6
131
+ signing_key:
132
132
  specification_version: 4
133
133
  summary: RSpec and Minitest matchers to prevent N+1 queries problem
134
134
  test_files: []