n_plus_one_control 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c24a275d72a208eda2b17b7eb6200eead0dc372a
4
+ data.tar.gz: 711bccd2b8d3cd2ebc337a8d9ebfd407933be10d
5
+ SHA512:
6
+ metadata.gz: e35efece4fd758e688bf3d294b26a7071e8d38a2ecfcda7fa3942130398a1578d8c539da171afb529dd4259fff692d42433197169c99b89dc2f12318a53efe7c
7
+ data.tar.gz: 83403b68d17b11f8c5261045d599ea64e5e33e1c1e7f4c1d1db345a12ef0b36dd3543866485e601e364a164104c678594b5452188d983fe5208d4e6499b8b8c3
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,71 @@
1
+ AllCops:
2
+ Include:
3
+ - 'lib/**/*.rb'
4
+ - 'lib/**/*.rake'
5
+ - 'spec/**/*.rb'
6
+ Exclude:
7
+ - 'bin/**/*'
8
+ - 'spec/dummy/**/*'
9
+ - 'tmp/**/*'
10
+ - 'Rakefile'
11
+ - 'Gemfile'
12
+ - '*.gemspec'
13
+ DisplayCopNames: true
14
+ StyleGuideCopsOnly: false
15
+ TargetRubyVersion: 2.4
16
+
17
+ Rails:
18
+ Enabled: false
19
+
20
+ Style/AccessorMethodName:
21
+ Enabled: false
22
+
23
+ Style/TrivialAccessors:
24
+ Enabled: false
25
+
26
+ Style/Documentation:
27
+ Exclude:
28
+ - 'spec/**/*.rb'
29
+ - 'tests/**/*.rb'
30
+
31
+ Style/StringLiterals:
32
+ Enabled: false
33
+
34
+ Style/RegexpLiteral:
35
+ Enabled: false
36
+
37
+ Style/SpaceInsideStringInterpolation:
38
+ EnforcedStyle: no_space
39
+
40
+ Style/ClassAndModuleChildren:
41
+ Enabled: false
42
+
43
+ Style/BlockDelimiters:
44
+ Exclude:
45
+ - 'spec/**/*.rb'
46
+
47
+ Lint/AmbiguousRegexpLiteral:
48
+ Enabled: false
49
+
50
+
51
+ Metrics/MethodLength:
52
+ Exclude:
53
+ - 'spec/**/*.rb'
54
+
55
+ Metrics/AbcSize:
56
+ Max: 20
57
+
58
+ Metrics/LineLength:
59
+ Max: 100
60
+ Exclude:
61
+ - 'spec/**/*.rb'
62
+
63
+ Metrics/BlockLength:
64
+ Exclude:
65
+ - 'spec/**/*.rb'
66
+
67
+ Rails/Date:
68
+ Enabled: false
69
+
70
+ Rails/TimeZone:
71
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.3
5
+ before_install: gem install bundler -v 1.13.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in n_plus_one_control.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 palkan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # N + 1 Control
2
+
3
+ RSpec and Minitest matchers to prevent the N+1 queries problem.
4
+
5
+ ### Why yet another gem to assert DB queries?
6
+
7
+ Unlike other libraries (such as [db-query-matchers](https://github.com/brigade/db-query-matchers), [rspec-sqlimit](https://github.com/nepalez/rspec-sqlimit), etc), with `n_plus_one_control` you don't have to specify exact expectations to control your code behaviour (e.g. `expect { subject }.to query(2).times`).
8
+
9
+ Such expectations are rather hard to maintain, 'cause there is a big chance of adding more queries, not related to the system under test.
10
+
11
+ NPlusOneControl works differently. It evaluates the code under consideration several times with different scale factors to make sure that the number of DB queries behaves as expected (i.e. O(1) instead of O(N)).
12
+
13
+ ### Why not just use [`bullet`](https://github.com/flyerhzm/bullet)?
14
+
15
+ 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.
16
+
17
+ This gem was born after I've found myself not able to verify with a test yet another N+1 problem.
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ group :test do
25
+ gem 'n_plus_one_control'
26
+ end
27
+ ```
28
+
29
+ And then execute:
30
+
31
+ $ bundle
32
+
33
+ ## Usage
34
+
35
+ ### RSpec
36
+
37
+ First, add NPlusOneControl to your `spec_helper.rb`:
38
+
39
+ ```ruby
40
+ # spec_helper.rb
41
+ ...
42
+
43
+ require "n_plus_one_control/rspec"
44
+ ```
45
+
46
+ Then:
47
+
48
+ ```ruby
49
+ # Wrap example into a context with :n_plus_one tag
50
+ context "N+1", :n_plus_one do
51
+ # Define `populate` callbacks which is responsible for data
52
+ # generation (and whatever else).
53
+ #
54
+ # It accepts one argument – the scale factor (read below)
55
+ populate { |n| create_list(:post, n) }
56
+
57
+ specify do
58
+ expect { get :index }.to perform_constant_number_of_queries
59
+ end
60
+ end
61
+ ```
62
+
63
+ **NOTE:** do not use memoized values within the expectation block!
64
+
65
+ ```ruby
66
+ # BAD – won't work!
67
+
68
+ subject { get :index }
69
+
70
+ specify do
71
+ expect { subject }.to perform_constant_number_of_queries
72
+ end
73
+ ```
74
+
75
+ Availables modifiers:
76
+
77
+ ```ruby
78
+ # You can specify the RegExp to filter queries.
79
+ # By default, it only considers SELECT queries.
80
+ expect { ... }.to perform_constant_number_of_queries.matching(/INSERT/)
81
+
82
+ # You can also provide custom scale factors
83
+ expect { ... }.to perform_constant_number_of_queries.with_scale_factors(10, 100)
84
+ ```
85
+
86
+ ### Minitest
87
+
88
+ First, add NPlusOneControl to your `test_helper.rb`:
89
+
90
+ ```ruby
91
+ # test_helper.rb
92
+ ...
93
+
94
+ require "n_plus_one_control/minitest"
95
+ ```
96
+
97
+ Then use `assert_perform_constant_number_of_queries` assertion method:
98
+
99
+ ```ruby
100
+ def test_no_n_plus_one_error
101
+ populate = ->(n) { create_list(:post, n) }
102
+
103
+ assert_perform_constant_number_of_queries(populate: populate) do
104
+ get :index
105
+ end
106
+ end
107
+ ```
108
+
109
+ You can also specify custom scale factors or filter patterns:
110
+
111
+ ```ruby
112
+ assert_perform_constant_number_of_queries(
113
+ populate: populate,
114
+ scale_factors: [2, 5, 10]
115
+ ) do
116
+ get :index
117
+ end
118
+
119
+ assert_perform_constant_number_of_queries(
120
+ populate: populate,
121
+ matching: /INSERT/
122
+ ) do
123
+ do_some_havey_stuff
124
+ end
125
+ ```
126
+
127
+ You can also specify `populate` as a test class instance method:
128
+
129
+ ```ruby
130
+ def populate(n)
131
+ create_list(:post, n)
132
+ end
133
+
134
+ def test_no_n_plus_one_error
135
+ assert_perform_constant_number_of_queries do
136
+ get :index
137
+ end
138
+ end
139
+ ```
140
+
141
+ ### Configuration
142
+
143
+ There are some global configuration parameters (and their corresponding defaults):
144
+
145
+ ```ruby
146
+ # Default scale factors to use.
147
+ # We use the smallest possible but representative scale factors by default.
148
+ self.default_scale_factors = [2, 3]
149
+
150
+ # Print performed queries if true in the case of failure
151
+ self.verbose = false
152
+
153
+ # Ignore matching queries
154
+ self.ignore = /^(BEGIN|COMMIT|SAVEPOINT|RELEASE)/
155
+
156
+ # ActiveSupport notifications event to track queries.
157
+ # We track ActiveRecord event by default,
158
+ # but can also track rom-rb events ('sql.rom') as well.
159
+ self.event = 'sql.active_record'
160
+ ```
161
+
162
+ ## How does it work?
163
+
164
+ Take a look at our [Executor](https://github.com/palkan/test-prof/tree/master/lib/n_plus_one_control/executor.rb) to figure out what's under the hood.
165
+
166
+ ## What's next?
167
+
168
+ It may be useful to provide more matchers/assertions, for example:
169
+
170
+ ```ruby
171
+
172
+ # Actually, that means that it is N+1))
173
+ assert_linear_number_of_queries { ... }
174
+
175
+ # But we can tune it with `coef` and handle such cases as selecting in batches
176
+ assert_linear_number_of_queries(coef: 0.1) do
177
+ Post.find_in_batches { ... }
178
+ end
179
+
180
+ # probably, also make sense to add another curve types
181
+ assert_logarithmic_number_of_queries { ... }
182
+ ```
183
+
184
+ If you want to discuss or implement any of these, feel free to open an [issue]() or propose a [pull request]().
185
+
186
+ ## Development
187
+
188
+ ```sh
189
+ # install deps
190
+ bundle install
191
+
192
+ # run tests
193
+ bundle exec rake
194
+ ```
195
+
196
+ ## Contributing
197
+
198
+ Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/n_plus_one_control.
199
+
200
+
201
+ ## License
202
+
203
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
204
+
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require "rubocop/rake_task"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.test_files = FileList['tests/**/*_test.rb']
8
+ end
9
+
10
+ RuboCop::RakeTask.new
11
+ RSpec::Core::RakeTask.new(:spec)
12
+
13
+ task :default => [:spec, :test, :rubocop]
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "n_plus_one_control"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "n_plus_one_control/version"
4
+ require "n_plus_one_control/executor"
5
+
6
+ # RSpec and Minitest matchers to prevent N+1 queries problem.
7
+ module NPlusOneControl
8
+ class << self
9
+ attr_accessor :default_scale_factors, :verbose, :ignore, :event
10
+
11
+ def failure_message(queries)
12
+ msg = ["Expected to make the same number of queries, but got:\n"]
13
+ queries.each do |(scale, data)|
14
+ msg << " #{data.size} for N=#{scale}\n"
15
+ msg << data.map { |sql| " #{sql}\n" }.join.to_s if verbose
16
+ end
17
+ msg.join
18
+ end
19
+ end
20
+
21
+ # Scale factors to use.
22
+ # Use the smallest possible but representative scale factors by default.
23
+ self.default_scale_factors = [2, 3]
24
+
25
+ # Print performed queries if true
26
+ self.verbose = false
27
+
28
+ # Ignore matching queries
29
+ self.ignore = /^(BEGIN|COMMIT|SAVEPOINT|RELEASE)/
30
+
31
+ # ActiveSupport notifications event to track queries.
32
+ # We track ActiveRecord event by default,
33
+ # but can also track rom-rb events ('sql.rom') as well.
34
+ self.event = 'sql.active_record'
35
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NPlusOneControl
4
+ # Runs code for every scale factor
5
+ # and returns collected queries.
6
+ module Executor
7
+ # Subscribes to ActiveSupport notifications and collect matching queries.
8
+ class Collector
9
+ def initialize(pattern)
10
+ @pattern = pattern
11
+ end
12
+
13
+ def call
14
+ @queries = []
15
+ ActiveSupport::Notifications
16
+ .subscribed(method(:callback), NPlusOneControl.event) do
17
+ yield
18
+ end
19
+ @queries
20
+ end
21
+
22
+ def callback(_name, _start, _finish, _message_id, values)
23
+ return if %w[CACHE SCHEMA].include? values[:name]
24
+ @queries << values[:sql] if @pattern.nil? || (values[:sql] =~ @pattern)
25
+ end
26
+ end
27
+
28
+ class << self
29
+ def call(population:, scale_factors: nil, matching: nil)
30
+ raise ArgumentError, "Block is required!" unless block_given?
31
+
32
+ results = []
33
+ collector = Collector.new(matching)
34
+
35
+ (scale_factors || NPlusOneControl.default_scale_factors).each do |scale|
36
+ with_transaction do
37
+ population.call(scale)
38
+ results << [scale, collector.call { yield }]
39
+ end
40
+ end
41
+ results
42
+ end
43
+
44
+ private
45
+
46
+ def with_transaction
47
+ return yield unless defined?(ActiveRecord)
48
+ ActiveRecord::Base.connection.begin_transaction(joinable: false)
49
+ yield
50
+ ensure
51
+ ActiveRecord::Base.connection.rollback_transaction
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "n_plus_one_control"
4
+
5
+ module NPlusOneControl
6
+ # Minitest assertions
7
+ module MinitestHelper
8
+ def assert_perform_constant_number_of_queries(
9
+ populate: nil,
10
+ matching: nil,
11
+ scale_factors: nil
12
+ )
13
+
14
+ raise ArgumentError, "Block is required" unless block_given?
15
+
16
+ queries = NPlusOneControl::Executor.call(
17
+ population: populate || method(:populate),
18
+ matching: matching || /^SELECT/i,
19
+ scale_factors: scale_factors || NPlusOneControl.default_scale_factors
20
+ ) { yield }
21
+
22
+ counts = queries.map(&:last).map(&:size)
23
+
24
+ assert counts.max == counts.min, NPlusOneControl.failure_message(queries)
25
+ end
26
+ end
27
+ end
28
+
29
+ Minitest::Test.include NPlusOneControl::MinitestHelper
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "n_plus_one_control"
4
+ require "n_plus_one_control/rspec/dsl"
5
+ require "n_plus_one_control/rspec/matcher"
6
+ require "n_plus_one_control/rspec/context"
7
+
8
+ module NPlusOneControl
9
+ module RSpec # :nodoc:
10
+ end
11
+ end
12
+
13
+ ::RSpec.configure do |config|
14
+ config.extend NPlusOneControl::RSpec::DSL, n_plus_one: true
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ ::RSpec.shared_context "n_plus_one_control", n_plus_one: true do
4
+ # Helper to access populate block from within example/matcher
5
+ let(:n_plus_one_populate) do |ex|
6
+ if ex.example_group.populate.nil?
7
+ raise(
8
+ <<~MSG
9
+ Populate block is missing!
10
+
11
+ Please provide populate callback, e.g.:
12
+
13
+ populate { |n| n.times { create_some_stuff } }
14
+ MSG
15
+ )
16
+ end
17
+ ->(n) { ex.instance_exec(n, &ex.example_group.populate) }
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NPlusOneControl
4
+ module RSpec
5
+ # Extends RSpec ExampleGroup with populate method
6
+ module DSL
7
+ # Setup populate callback, which is used
8
+ # to prepare data for each run.
9
+ def populate
10
+ return @populate unless block_given?
11
+
12
+ @populate = Proc.new
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ ::RSpec::Matchers.define :perform_constant_number_of_queries do
5
+ supports_block_expectations
6
+
7
+ chain :with_scale_factors do |*factors|
8
+ @factors = factors
9
+ end
10
+
11
+ chain :matching do |pattern|
12
+ @pattern = pattern
13
+ end
14
+
15
+ match do |actual, *_args|
16
+ raise ArgumentError, "Block is required" unless actual.is_a? Proc
17
+
18
+ raise "Missing tag :n_plus_one" unless
19
+ @matcher_execution_context.respond_to?(:n_plus_one_populate)
20
+
21
+ populate = @matcher_execution_context.n_plus_one_populate
22
+
23
+ # by default we're looking for select queries
24
+ pattern = @pattern || /^SELECT/i
25
+
26
+ @queries = NPlusOneControl::Executor.call(
27
+ population: populate,
28
+ matching: pattern,
29
+ scale_factors: @factors,
30
+ &actual
31
+ )
32
+
33
+ counts = @queries.map(&:last).map(&:size)
34
+
35
+ counts.max == counts.min
36
+ end
37
+
38
+ match_when_negated do |_actual|
39
+ raise "This matcher doesn't support negation"
40
+ end
41
+
42
+ failure_message { |_actual| NPlusOneControl.failure_message(@queries) }
43
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NPlusOneControl
4
+ VERSION = "0.0.3"
5
+ end
@@ -0,0 +1,46 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'n_plus_one_control/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "n_plus_one_control"
8
+ spec.version = NPlusOneControl::VERSION
9
+ spec.authors = ["palkan"]
10
+ spec.email = ["dementiev.vm@gmail.com"]
11
+
12
+ spec.summary = "RSpec and Minitest matchers to prevent N+1 queries problem"
13
+ spec.description = %{
14
+ RSpec and Minitest matchers to prevent N+1 queries problem.
15
+
16
+ Evaluates code under consideration several times with different scale factors
17
+ to make sure that the number of DB queries behaves as expected (i.e. O(1) instead of O(N)).
18
+
19
+ Example:
20
+
21
+ ```ruby
22
+ context "N+1", :n_plus_one do
23
+ populate { |n| create_list(:post, n) }
24
+
25
+ specify do
26
+ expect { get :index }.to perform_constant_number_of_queries
27
+ end
28
+ end
29
+ ```
30
+ }
31
+ spec.homepage = "http://github.com/palkan/n_plus_one_control"
32
+ spec.license = "MIT"
33
+
34
+ spec.files = `git ls-files`.split($/)
35
+ spec.require_paths = ["lib"]
36
+
37
+ spec.add_development_dependency "bundler", "~> 1.10"
38
+ spec.add_development_dependency "rake", "~> 10.0"
39
+ spec.add_development_dependency "rspec", "~> 3.5"
40
+ spec.add_development_dependency "minitest", "~> 5.9"
41
+ spec.add_development_dependency "factory_girl", "~> 4.8.0"
42
+ spec.add_development_dependency "rubocop", "~> 0.49"
43
+ spec.add_development_dependency "activerecord", "~> 5.1"
44
+ spec.add_development_dependency "sqlite3"
45
+ spec.add_development_dependency "pry-byebug"
46
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe NPlusOneControl::Executor do
6
+ let(:populate) do
7
+ ->(n) { create_list(:post, n) }
8
+ end
9
+
10
+ let(:observable) do
11
+ -> { Post.find_each(&:user) }
12
+ end
13
+
14
+ it "raises when block is missing" do
15
+ expect { described_class.call(population: populate) }
16
+ .to raise_error(ArgumentError, "Block is required!")
17
+ end
18
+
19
+ it "raises when populate is missing" do
20
+ expect { described_class.call(&observable) }
21
+ .to raise_error(ArgumentError, /population/)
22
+ end
23
+
24
+ it "returns correct counts for default scales" do
25
+ result = described_class.call(
26
+ population: populate,
27
+ &observable
28
+ )
29
+
30
+ expect(result.size).to eq 2
31
+ expect(result.first[0]).to eq 2
32
+ expect(result.first[1].size).to eq 3
33
+ expect(result.last[0]).to eq 3
34
+ expect(result.last[1].size).to eq 4
35
+ end
36
+
37
+ it "returns correct counts for custom scales" do
38
+ result = described_class.call(
39
+ population: populate,
40
+ scale_factors: [5, 10, 100],
41
+ &observable
42
+ )
43
+
44
+ expect(result.size).to eq 3
45
+ expect(result.first[0]).to eq 5
46
+ expect(result.first[1].size).to eq 6
47
+ expect(result.second[0]).to eq 10
48
+ expect(result.second[1].size).to eq 11
49
+ expect(result.last[0]).to eq 100
50
+ expect(result.last[1].size).to eq 101
51
+ end
52
+
53
+ it "returns correct counts with custom match" do
54
+ result = described_class.call(
55
+ population: populate,
56
+ matching: /users/,
57
+ &observable
58
+ )
59
+
60
+ expect(result.first[0]).to eq 2
61
+ expect(result.first[1].size).to eq 2
62
+ expect(result.last[0]).to eq 3
63
+ expect(result.last[1].size).to eq 3
64
+ end
65
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe NPlusOneControl::RSpec do
6
+ context "when no N+1", :n_plus_one do
7
+ populate { |n| create_list(:post, n) }
8
+
9
+ specify do
10
+ expect { Post.preload(:user).find_each { |p| p.user.name } }
11
+ .to perform_constant_number_of_queries
12
+ end
13
+ end
14
+
15
+ context "when has N+1", :n_plus_one do
16
+ populate { |n| create_list(:post, n) }
17
+
18
+ specify do
19
+ expect do
20
+ expect { Post.find_each { |p| p.user.name } }
21
+ .to perform_constant_number_of_queries
22
+ end.to raise_error(RSpec::Expectations::ExpectationNotMetError)
23
+ end
24
+ end
25
+
26
+ context "when context is missing" do
27
+ specify do
28
+ expect do
29
+ expect { subject }.to perform_constant_number_of_queries
30
+ end.to raise_error(/missing tag/i)
31
+ end
32
+ end
33
+
34
+ context "when populate is missing", :n_plus_one do
35
+ specify do
36
+ expect do
37
+ expect { subject }.to perform_constant_number_of_queries
38
+ end.to raise_error(/please provide populate/i)
39
+ end
40
+ end
41
+
42
+ context "when negated" do
43
+ specify do
44
+ expect do
45
+ expect { subject }.not_to perform_constant_number_of_queries
46
+ end.to raise_error(/support negation/i)
47
+ end
48
+ end
49
+
50
+ context "when verbose", :n_plus_one do
51
+ populate { |n| create_list(:post, n) }
52
+
53
+ around(:each) do |ex|
54
+ NPlusOneControl.verbose = true
55
+ ex.run
56
+ NPlusOneControl.verbose = false
57
+ end
58
+
59
+ specify do
60
+ expect do
61
+ expect { Post.find_each { |p| p.user.name } }
62
+ .to perform_constant_number_of_queries
63
+ end.to raise_error(RSpec::Expectations::ExpectationNotMetError, /select .+ from/i)
64
+ end
65
+ end
66
+
67
+ context "with scale_factors", :n_plus_one do
68
+ populate { |n| create_list(:post, n) }
69
+
70
+ specify do
71
+ expect { Post.find_each { |p| p.user.name } }
72
+ .to perform_constant_number_of_queries.with_scale_factors(1, 1)
73
+ end
74
+ end
75
+
76
+ context "with matching", :n_plus_one do
77
+ populate { |n| create_list(:post, n) }
78
+
79
+ specify do
80
+ expect { Post.find_each { |p| p.user.name } }
81
+ .to perform_constant_number_of_queries.matching(/posts/)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe NPlusOneControl do
6
+ it "has a version number" do
7
+ expect(NPlusOneControl::VERSION).not_to be nil
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
4
+ require "n_plus_one_control/rspec"
5
+ require "benchmark"
6
+ require "active_record"
7
+ require "factory_girl"
8
+ require "pry-byebug"
9
+
10
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
11
+
12
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
13
+
14
+ RSpec.configure do |config|
15
+ config.mock_with :rspec
16
+
17
+ config.order = :random
18
+ config.filter_run focus: true
19
+ config.run_all_when_everything_filtered = true
20
+
21
+ config.include FactoryGirl::Syntax::Methods
22
+
23
+ config.before(:each) do
24
+ ActiveRecord::Base.connection.begin_transaction(joinable: false)
25
+ end
26
+
27
+ config.after(:each) do
28
+ ActiveRecord::Base.connection.rollback_transaction
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveRecord::Schema.define do
4
+ create_table :posts do |t|
5
+ t.string :title
6
+ t.integer :user_id
7
+ end
8
+ end
9
+
10
+ class Post < ActiveRecord::Base
11
+ belongs_to :user
12
+ end
13
+
14
+ FactoryGirl.define do
15
+ factory :post do
16
+ title "Title"
17
+ user
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveRecord::Schema.define do
4
+ create_table :users do |t|
5
+ t.string :name
6
+ end
7
+ end
8
+
9
+ class User < ActiveRecord::Base
10
+ has_many :posts
11
+ end
12
+
13
+ FactoryGirl.define do
14
+ factory :user do
15
+ name "John"
16
+ end
17
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class TestMinitest < Minitest::Test
6
+ def test_no_n_plus_one_error
7
+ populate = ->(n) { create_list(:post, n) }
8
+
9
+ assert_perform_constant_number_of_queries(populate: populate) do
10
+ Post.preload(:user).find_each { |p| p.user.name }
11
+ end
12
+ end
13
+
14
+ def test_with_n_plus_one_error
15
+ populate = ->(n) { create_list(:post, n) }
16
+
17
+ e = assert_raises Minitest::Assertion do
18
+ assert_perform_constant_number_of_queries(populate: populate) do
19
+ Post.find_each { |p| p.user.name }
20
+ end
21
+ end
22
+
23
+ assert_match "Expected to make the same number of queries", e.message
24
+ assert_match "3 for N=2", e.message
25
+ assert_match "4 for N=3", e.message
26
+ end
27
+
28
+ def test_no_n_plus_one_error_with_scale_factors
29
+ populate = ->(n) { create_list(:post, n) }
30
+
31
+ assert_perform_constant_number_of_queries(
32
+ populate: populate,
33
+ scale_factors: [1, 1]
34
+ ) do
35
+ Post.find_each { |p| p.user.name }
36
+ end
37
+ end
38
+
39
+ def test_no_n_plus_one_error_with_matching
40
+ populate = ->(n) { create_list(:post, n) }
41
+
42
+ assert_perform_constant_number_of_queries(
43
+ populate: populate,
44
+ matching: /posts/
45
+ ) do
46
+ Post.find_each { |p| p.user.name }
47
+ end
48
+ end
49
+
50
+ def populate(n)
51
+ create_list(:post, n)
52
+ end
53
+
54
+ def test_fallback_to_populate_method
55
+ e = assert_raises Minitest::Assertion do
56
+ assert_perform_constant_number_of_queries do
57
+ Post.find_each { |p| p.user.name }
58
+ end
59
+ end
60
+
61
+ assert_match "Expected to make the same number of queries", e.message
62
+ end
63
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "minitest/pride"
5
+
6
+ $LOAD_PATH << File.expand_path("../../lib", __FILE__)
7
+ Thread.abort_on_exception = true
8
+
9
+ require "n_plus_one_control/minitest"
10
+ require "benchmark"
11
+ require "active_record"
12
+ require "factory_girl"
13
+ require "pry-byebug"
14
+
15
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
16
+
17
+ Dir["#{File.dirname(__FILE__)}/../spec/support/**/*.rb"].each { |f| require f }
18
+
19
+ module TransactionalTests
20
+ def setup
21
+ ActiveRecord::Base.connection.begin_transaction(joinable: false)
22
+ super
23
+ end
24
+
25
+ def teardown
26
+ super
27
+ ActiveRecord::Base.connection.rollback_transaction
28
+ end
29
+ end
30
+
31
+ Minitest::Test.prepend TransactionalTests
32
+ Minitest::Test.include FactoryGirl::Syntax::Methods
metadata ADDED
@@ -0,0 +1,203 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: n_plus_one_control
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - palkan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-06-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.9'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.9'
69
+ - !ruby/object:Gem::Dependency
70
+ name: factory_girl
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 4.8.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 4.8.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.49'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.49'
97
+ - !ruby/object:Gem::Dependency
98
+ name: activerecord
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '5.1'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '5.1'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sqlite3
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry-byebug
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: "\n RSpec and Minitest matchers to prevent N+1 queries problem.\n\n
140
+ \ Evaluates code under consideration several times with different scale factors\n
141
+ \ to make sure that the number of DB queries behaves as expected (i.e. O(1) instead
142
+ of O(N)).\n\n Example:\n\n ```ruby\n context \"N+1\", :n_plus_one do\n
143
+ \ populate { |n| create_list(:post, n) }\n\n specify do\n expect
144
+ { get :index }.to perform_constant_number_of_queries\n end\n end\n ```\n
145
+ \ "
146
+ email:
147
+ - dementiev.vm@gmail.com
148
+ executables: []
149
+ extensions: []
150
+ extra_rdoc_files: []
151
+ files:
152
+ - ".gitignore"
153
+ - ".rspec"
154
+ - ".rubocop.yml"
155
+ - ".travis.yml"
156
+ - Gemfile
157
+ - LICENSE.txt
158
+ - README.md
159
+ - Rakefile
160
+ - bin/console
161
+ - bin/setup
162
+ - lib/n_plus_one_control.rb
163
+ - lib/n_plus_one_control/executor.rb
164
+ - lib/n_plus_one_control/minitest.rb
165
+ - lib/n_plus_one_control/rspec.rb
166
+ - lib/n_plus_one_control/rspec/context.rb
167
+ - lib/n_plus_one_control/rspec/dsl.rb
168
+ - lib/n_plus_one_control/rspec/matcher.rb
169
+ - lib/n_plus_one_control/version.rb
170
+ - n_plus_one_control.gemspec
171
+ - spec/n_plus_one_control/executor_spec.rb
172
+ - spec/n_plus_one_control/rspec_spec.rb
173
+ - spec/n_plus_one_control_spec.rb
174
+ - spec/spec_helper.rb
175
+ - spec/support/post.rb
176
+ - spec/support/user.rb
177
+ - tests/minitest_test.rb
178
+ - tests/test_helper.rb
179
+ homepage: http://github.com/palkan/n_plus_one_control
180
+ licenses:
181
+ - MIT
182
+ metadata: {}
183
+ post_install_message:
184
+ rdoc_options: []
185
+ require_paths:
186
+ - lib
187
+ required_ruby_version: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - ">="
190
+ - !ruby/object:Gem::Version
191
+ version: '0'
192
+ required_rubygems_version: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - ">="
195
+ - !ruby/object:Gem::Version
196
+ version: '0'
197
+ requirements: []
198
+ rubyforge_project:
199
+ rubygems_version: 2.6.4
200
+ signing_key:
201
+ specification_version: 4
202
+ summary: RSpec and Minitest matchers to prevent N+1 queries problem
203
+ test_files: []