rspec-activerecord-expectations 1.0.1 → 2.1.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/.github/workflows/main.yml +41 -12
- data/CHANGELOG.md +25 -2
- data/README.md +91 -24
- data/lib/rspec/activerecord/expectations/collector.rb +25 -6
- data/lib/rspec/activerecord/expectations/matchers/load_matcher.rb +29 -0
- data/lib/rspec/activerecord/expectations/matchers/query_count_matcher.rb +141 -0
- data/lib/rspec/activerecord/expectations/query_inspector.rb +28 -0
- data/lib/rspec/activerecord/expectations.rb +7 -1
- data/rspec-activerecord-expectations.gemspec +8 -3
- metadata +19 -17
- data/lib/rspec/activerecord/expectations/matchers/query_count.rb +0 -57
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d320c1e0f80e96c3bbb2cb07df63b941aa9bb1aa3878f9ca46e6fa1fed483569
|
4
|
+
data.tar.gz: 69487efb7a89d208d28a0d26faa3c74fc3315645803a1ec4162a6bd9f3677008
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7db71325cbfa0abd65c7128cceb18ba8017af399bee5cdd4502d84a988c3865605f0c5d34f858af352c1520b401651723fbf1c2335d652ec45f9ef0308a675bb
|
7
|
+
data.tar.gz: 44082fc8ad08369ea9e578f6cb3a11ec3dd7f92445b63ef4d415e10e14c79e350ded3a39828821b613513c382fd71b72db9b0669e7c14bd2e900655e03cffa5d
|
data/.github/workflows/main.yml
CHANGED
@@ -11,22 +11,22 @@ jobs:
|
|
11
11
|
strategy:
|
12
12
|
matrix:
|
13
13
|
os: [ubuntu-latest, macos-latest]
|
14
|
-
|
14
|
+
ruby-version: ['3.1', '3.0', '2.7']
|
15
15
|
rails: ['5.0', '5.1', '5.2', '6.0', '6.1', '7.0']
|
16
16
|
|
17
17
|
runs-on: ${{ matrix.os }}
|
18
|
-
name:
|
18
|
+
name: ruby ${{ matrix.ruby-version }}, rails ${{ matrix.rails }}, ${{ matrix.os }}
|
19
19
|
|
20
20
|
steps:
|
21
21
|
- uses: actions/checkout@v2
|
22
22
|
|
23
|
-
- name: Set up Ruby ${{ matrix.
|
23
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
24
24
|
uses: ruby/setup-ruby@v1
|
25
25
|
with:
|
26
|
-
|
26
|
+
ruby-version: ${{ matrix.ruby-version }}
|
27
27
|
bundler-cache: true
|
28
28
|
|
29
|
-
- name: Install
|
29
|
+
- name: Install Gems w/ Rails ${{ matrix.rails }}
|
30
30
|
env:
|
31
31
|
MATRIX_RAILS_VERSION: ${{ matrix.rails }}
|
32
32
|
run: |
|
@@ -40,23 +40,51 @@ jobs:
|
|
40
40
|
test_legacy:
|
41
41
|
strategy:
|
42
42
|
matrix:
|
43
|
-
|
44
|
-
ruby_version: ['2.6', '2.5', '2.4', '2.3']
|
43
|
+
ruby-version: ['2.6', '2.5']
|
45
44
|
rails: ['5.0', '5.1', '5.2']
|
46
45
|
|
47
|
-
runs-on:
|
48
|
-
name:
|
46
|
+
runs-on: ubuntu-latest
|
47
|
+
name: ruby ${{ matrix.ruby-version }}, rails ${{ matrix.rails }}
|
48
|
+
|
49
|
+
steps:
|
50
|
+
- uses: actions/checkout@v2
|
51
|
+
|
52
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
53
|
+
uses: ruby/setup-ruby@v1
|
54
|
+
with:
|
55
|
+
ruby-version: ${{ matrix.ruby-version }}
|
56
|
+
bundler-cache: true
|
57
|
+
|
58
|
+
- name: Install Gems w/ Rails ${{ matrix.rails }}
|
59
|
+
env:
|
60
|
+
MATRIX_RAILS_VERSION: ${{ matrix.rails }}
|
61
|
+
run: |
|
62
|
+
export BUNDLE_GEMFILE="${GITHUB_WORKSPACE}/gemfiles/rails_${MATRIX_RAILS_VERSION}.gemfile"
|
63
|
+
gem install bundler
|
64
|
+
bundle install --jobs 4 --retry 3
|
65
|
+
|
66
|
+
- name: Run tests
|
67
|
+
run: bundle exec rspec
|
68
|
+
|
69
|
+
test_alt_rubies:
|
70
|
+
strategy:
|
71
|
+
matrix:
|
72
|
+
ruby-version: ['jruby', 'truffleruby']
|
73
|
+
rails: ['6.0', '6.1'] # jruby / jdbc-sqlite3 end up tightly bound to rails 6.x
|
74
|
+
|
75
|
+
runs-on: ubuntu-latest
|
76
|
+
name: ${{ matrix.ruby-version }}, rails ${{ matrix.rails }}
|
49
77
|
|
50
78
|
steps:
|
51
79
|
- uses: actions/checkout@v2
|
52
80
|
|
53
|
-
- name: Set up Ruby ${{ matrix.
|
81
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
54
82
|
uses: ruby/setup-ruby@v1
|
55
83
|
with:
|
56
|
-
|
84
|
+
ruby-version: ${{ matrix.ruby-version }}
|
57
85
|
bundler-cache: true
|
58
86
|
|
59
|
-
- name: Install
|
87
|
+
- name: Install Gems w/ Rails ${{ matrix.rails }}
|
60
88
|
env:
|
61
89
|
MATRIX_RAILS_VERSION: ${{ matrix.rails }}
|
62
90
|
run: |
|
@@ -66,3 +94,4 @@ jobs:
|
|
66
94
|
|
67
95
|
- name: Run tests
|
68
96
|
run: bundle exec rspec
|
97
|
+
|
data/CHANGELOG.md
CHANGED
@@ -1,10 +1,33 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
## [1.0
|
3
|
+
## [2.1.0] - 2022-01-12
|
4
|
+
- change CI matrix for more version compatibility
|
5
|
+
- update gemspec to move some deps into development only
|
6
|
+
- README changes
|
7
|
+
|
8
|
+
## [2.0.0] - 2022-01-07
|
9
|
+
- Require much more recent ruby, to use syntax that's been around since 2017
|
10
|
+
|
11
|
+
## [1.3.0] - 2021-12-31
|
12
|
+
- Add `repeatedly_load` matcher
|
13
|
+
- Add query type matchers for `load_queries`, `schema_queries`, `transaction_queries`, `destroy_queries`
|
14
|
+
- Allow singular version of all query types (e.g. `transaction_queries` vs `transaction_query`)
|
15
|
+
- Fix failure message for `execute.exactly` matcher
|
16
|
+
|
17
|
+
## [1.2.0] - 2021-12-31
|
18
|
+
- Add `query` as a synonym for `queries`
|
19
|
+
- Ignore schema and transaction queries in query count
|
20
|
+
- Add beginning of recording specific query types
|
21
|
+
- Add query count matcher for `exactly`
|
22
|
+
|
23
|
+
## [1.1.0] - 2021-12-30
|
24
|
+
- Add query count matchers for e.g. `less_than_or_equal_to`, `greater_than`
|
25
|
+
|
26
|
+
## [1.0.1] - 2021-12-30
|
4
27
|
- Pin all the dependencies to a proper working subset
|
5
28
|
- Expand testing to many rails / ruby combinations
|
6
29
|
|
7
|
-
## [1.0.0] -
|
30
|
+
## [1.0.0] - 2021-12-30
|
8
31
|
- Basic gem w/ all the fixins and README and such
|
9
32
|
- Add `less_than` comparison, and `queries` type
|
10
33
|
- Basic tests in place, and not bad tbh
|
data/README.md
CHANGED
@@ -30,7 +30,8 @@ group 'test' do
|
|
30
30
|
end
|
31
31
|
```
|
32
32
|
|
33
|
-
Then, include the functionality within your
|
33
|
+
Then, include the functionality within your `rails_helper.rb` (or in a support
|
34
|
+
file).
|
34
35
|
|
35
36
|
```ruby
|
36
37
|
RSpec.configure do |config|
|
@@ -91,11 +92,91 @@ That's it! Refactor your report to be more efficient and then leave the test in
|
|
91
92
|
place to make sure that future developers don't accidentally cause a
|
92
93
|
performance regression.
|
93
94
|
|
94
|
-
##
|
95
|
+
## Preventing Repeated Load (N+1) Queries
|
96
|
+
|
97
|
+
Reloading, whether by e.g. `Album.find` or `album.tracks` are both antipatterns
|
98
|
+
within your code. They will load from the database for every iteration in a
|
99
|
+
loop, unless you load records outside the loop, cache responses, or use an
|
100
|
+
eager loading mechanism like `includes`. These sorts of queries are often
|
101
|
+
referred to as N+1 queries.
|
102
|
+
|
103
|
+
This sort of query can be prevented using the `repeatedly_load` expectation.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
expect {}.not_to repeatedly_load('SomeActiveRecordClass')
|
107
|
+
```
|
108
|
+
|
109
|
+
This matcher will track ActiveRecord's built in load methods to prevent those
|
110
|
+
N+1 situations. Using eager loading (e.g. `Track.all.includes(:album)`) will
|
111
|
+
allow these expectations to pass as expected!
|
112
|
+
|
113
|
+
### Testing Batch Queries
|
114
|
+
|
115
|
+
If your code loads records in batches, it may be more difficult to create
|
116
|
+
expectations for repeated loading. After all, each batch will execute its own
|
117
|
+
queries, which may look like repeated loading.
|
118
|
+
|
119
|
+
If your test has a small enough number of records that only one batch is
|
120
|
+
loaded, your tests may work just fine. But otherwise, you may want to allow
|
121
|
+
your code to specify a batch size in order to guarantee only a single batch
|
122
|
+
is loaded:
|
95
123
|
|
96
124
|
```ruby
|
125
|
+
tracks = Track.all
|
126
|
+
|
127
|
+
expect {
|
128
|
+
TrackSerializer.perform(tracks, batch_size: tracks.count)
|
129
|
+
}.not_to repeatedly_load('Track')
|
130
|
+
```
|
131
|
+
|
132
|
+
## Counting Queries
|
133
|
+
|
134
|
+
### Types of Comparisons
|
135
|
+
|
136
|
+
Several comparison types are available, along with some aliases to allow for
|
137
|
+
easier to read tests.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
expect {}.to execute.less_than(20).queries
|
97
141
|
expect {}.to execute.fewer_than(20).queries
|
98
|
-
|
142
|
+
|
143
|
+
expect {}.to execute.less_than_or_equal_to(20).queries
|
144
|
+
expect {}.to execute.at_most(20).queries
|
145
|
+
|
146
|
+
expect {}.to execute.greater_than(20).queries
|
147
|
+
expect {}.to execute.more_than(20).queries
|
148
|
+
|
149
|
+
expect {}.to execute.greater_than_or_equal_to(20).queries
|
150
|
+
expect {}.to execute.at_least(20).queries
|
151
|
+
|
152
|
+
expect {}.to execute.exactly(20).queries
|
153
|
+
|
154
|
+
expect {}.to execute.at_least(2).queries
|
155
|
+
expect {}.to execute.at_least(1).query # singular form also accepted
|
156
|
+
```
|
157
|
+
|
158
|
+
### Specific Query Types
|
159
|
+
|
160
|
+
You can of course make assertions for the total number of queries executed, but
|
161
|
+
sometimes it's more valuable to assert particular _types_ of queries, such as
|
162
|
+
inserts or find statements. Matchers are supported for this as well!
|
163
|
+
|
164
|
+
**Note:** Transaction (for example, `ROLLBACK`) queries are not counted in any of these
|
165
|
+
categories, nor are queries that load the DB schema.
|
166
|
+
|
167
|
+
**Note:** Destroy and delete queries are both condensed into the matcher for
|
168
|
+
`destroy_queries`.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
expect {}.to execute.exactly(20).queries
|
172
|
+
|
173
|
+
expect {}.to execute.exactly(20).insert_queries
|
174
|
+
expect {}.to execute.exactly(20).load_queries
|
175
|
+
expect {}.to execute.exactly(20).destroy_queries
|
176
|
+
expect {}.to execute.exactly(20).exists_queries
|
177
|
+
|
178
|
+
expect {}.to execute.exactly(20).schema_queries
|
179
|
+
expect {}.to execute.exactly(20).transaction_queries
|
99
180
|
```
|
100
181
|
|
101
182
|
## Future Planned Functionality
|
@@ -103,17 +184,11 @@ expect {}.to execute.less_than(20).queries # alias for fewer_than
|
|
103
184
|
This gem still has lots of future functionality. See below.
|
104
185
|
|
105
186
|
```ruby
|
106
|
-
expect {}.to execute.
|
107
|
-
expect {}.to execute.
|
108
|
-
|
109
|
-
expect {}.to execute.at_least(
|
110
|
-
|
111
|
-
expect {}.to execute.less_than(20).delete_queries
|
112
|
-
expect {}.to execute.less_than(20).load_queries
|
113
|
-
expect {}.to execute.less_than(20).schema_queries
|
114
|
-
expect {}.to execute.less_than(20).exists_queries
|
115
|
-
expect {}.to execute.less_than(20).queries
|
116
|
-
expect {}.to execute.less_than(20).queries_of_type("Audited::Audit Load")
|
187
|
+
expect {}.to execute.at_least(2).queries_of_type("Audited::Audit Load")
|
188
|
+
expect {}.to execute.at_least(2).load_queries("Audited::Audit")
|
189
|
+
|
190
|
+
expect {}.to execute.at_least(2).activerecord_queries
|
191
|
+
expect {}.to execute.at_least(2).hand_rolled_queries
|
117
192
|
|
118
193
|
expect {}.not_to rollback_transaction.exactly(5).times
|
119
194
|
expect {}.not_to commit_transaction.once
|
@@ -123,18 +198,10 @@ expect {}.to create.exactly(5).of_type(User)
|
|
123
198
|
expect {}.to insert.exactly(5).subscription_changes
|
124
199
|
expect {}.to update.exactly(2).of_any_type
|
125
200
|
expect {}.to delete.exactly(2).of_any_type
|
126
|
-
|
127
|
-
expect {}.not_to repeatedly_load(Audited::Audit)
|
128
|
-
|
129
|
-
at_least, more_than, at_most, less_than, lteq?
|
130
|
-
exactly(1) => once?
|
131
201
|
```
|
132
202
|
|
133
|
-
-
|
134
|
-
-
|
135
|
-
- differentiate AR queries from generic ones? arbitrary execution somehow?
|
136
|
-
- warn about warmup
|
137
|
-
- make sure we don't smite any built in methods (or from other libs)
|
203
|
+
- warn if we smite any built in methods (or methods from other libs)
|
204
|
+
- support Rails 6 bulk insert (still one query)
|
138
205
|
|
139
206
|
## Development
|
140
207
|
|
@@ -1,23 +1,42 @@
|
|
1
1
|
module RSpec::ActiveRecord::Expectations
|
2
2
|
class Collector
|
3
3
|
def initialize
|
4
|
-
@
|
5
|
-
|
6
|
-
}
|
4
|
+
@inspector = QueryInspector.new
|
5
|
+
@by_name = {}
|
6
|
+
@counts = QueryInspector.valid_query_types.each_with_object({}) do |query_type, hash|
|
7
|
+
hash[query_type] = 0
|
8
|
+
end
|
7
9
|
|
8
|
-
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)
|
9
15
|
end
|
10
16
|
|
11
17
|
def queries_of_type(type)
|
12
|
-
@counts
|
18
|
+
@counts[type] || (raise ArgumentError, "Sorry, #{type} is not a valid kind of query")
|
13
19
|
end
|
14
20
|
|
15
21
|
def valid_type?(type)
|
16
22
|
@counts.include? type
|
17
23
|
end
|
18
24
|
|
25
|
+
def calls_by_name(name)
|
26
|
+
@by_name.fetch(name, 0)
|
27
|
+
end
|
28
|
+
|
19
29
|
def record_query(*_unused, data)
|
20
|
-
|
30
|
+
categories = @inspector.categorize(data)
|
31
|
+
|
32
|
+
categories.each do |category|
|
33
|
+
@counts[category] += 1
|
34
|
+
rescue NoMethodError
|
35
|
+
raise "tried to add to to #{category} but it doesn't exist"
|
36
|
+
end
|
37
|
+
|
38
|
+
@by_name[data[:name]] ||= 0
|
39
|
+
@by_name[data[:name]] += 1
|
21
40
|
end
|
22
41
|
end
|
23
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
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module RSpec::ActiveRecord::Expectations
|
2
|
+
module Matchers
|
3
|
+
class QueryCountMatcher
|
4
|
+
attr_reader :failure_message, :failure_message_when_negated
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@collector = Collector.new
|
8
|
+
|
9
|
+
@match_method = nil
|
10
|
+
@comparison = nil
|
11
|
+
@query_type = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def supports_block_expectations?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def matches?(block)
|
19
|
+
raise NoComparisonError unless @match_method
|
20
|
+
raise NoQueryTypeError unless @collector.valid_type?(@query_type)
|
21
|
+
|
22
|
+
block.call
|
23
|
+
result = @match_method.call
|
24
|
+
@collector.finalize
|
25
|
+
|
26
|
+
result
|
27
|
+
end
|
28
|
+
|
29
|
+
# COMPARISON TYPES
|
30
|
+
|
31
|
+
def less_than(n)
|
32
|
+
@comparison = n
|
33
|
+
@match_method = method(:compare_less_than)
|
34
|
+
self
|
35
|
+
end
|
36
|
+
alias_method :fewer_than, :less_than
|
37
|
+
|
38
|
+
def less_than_or_equal_to(n)
|
39
|
+
@comparison = n
|
40
|
+
@match_method = method(:compare_less_than_or_equal_to)
|
41
|
+
self
|
42
|
+
end
|
43
|
+
alias_method :at_most, :less_than_or_equal_to
|
44
|
+
|
45
|
+
def greater_than(n)
|
46
|
+
@comparison = n
|
47
|
+
@match_method = method(:compare_greater_than)
|
48
|
+
self
|
49
|
+
end
|
50
|
+
alias_method :more_than, :greater_than
|
51
|
+
|
52
|
+
def greater_than_or_equal_to(n)
|
53
|
+
@comparison = n
|
54
|
+
@match_method = method(:compare_greater_than_or_equal_to)
|
55
|
+
self
|
56
|
+
end
|
57
|
+
alias_method :at_least, :greater_than_or_equal_to
|
58
|
+
|
59
|
+
def exactly(n)
|
60
|
+
@comparison = n
|
61
|
+
@match_method = method(:compare_exactly)
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
# TARGET QUERY TYPES
|
66
|
+
|
67
|
+
RSpec::ActiveRecord::Expectations::QueryInspector.valid_query_types.each do |type|
|
68
|
+
define_method type do
|
69
|
+
@query_type = type
|
70
|
+
self
|
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
|
80
|
+
end
|
81
|
+
|
82
|
+
# TODO singularize everything
|
83
|
+
alias_method :query, :queries
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def humanized_query_type
|
88
|
+
@query_type.to_s.gsub("_", " ")
|
89
|
+
end
|
90
|
+
|
91
|
+
# MATCHERS
|
92
|
+
|
93
|
+
def compare_less_than
|
94
|
+
count = @collector.queries_of_type(@query_type)
|
95
|
+
|
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}"
|
98
|
+
|
99
|
+
count < @comparison
|
100
|
+
end
|
101
|
+
|
102
|
+
def compare_less_than_or_equal_to
|
103
|
+
count = @collector.queries_of_type(@query_type)
|
104
|
+
|
105
|
+
# boy this negated operator is confusing. don't use that plz.
|
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}"
|
108
|
+
|
109
|
+
count <= @comparison
|
110
|
+
end
|
111
|
+
|
112
|
+
def compare_greater_than
|
113
|
+
count = @collector.queries_of_type(@query_type)
|
114
|
+
|
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}"
|
117
|
+
|
118
|
+
count > @comparison
|
119
|
+
end
|
120
|
+
|
121
|
+
def compare_greater_than_or_equal_to
|
122
|
+
count = @collector.queries_of_type(@query_type)
|
123
|
+
|
124
|
+
# see above, negating this matcher is so confusing.
|
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}"
|
127
|
+
|
128
|
+
count >= @comparison
|
129
|
+
end
|
130
|
+
|
131
|
+
def compare_exactly
|
132
|
+
count = @collector.queries_of_type(@query_type)
|
133
|
+
|
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}"
|
136
|
+
|
137
|
+
count == @comparison
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module RSpec::ActiveRecord::Expectations
|
2
|
+
class QueryInspector
|
3
|
+
def self.valid_query_types
|
4
|
+
[:queries, :schema_queries, :transaction_queries, :insert_queries,
|
5
|
+
:load_queries, :destroy_queries, :exists_queries]
|
6
|
+
end
|
7
|
+
|
8
|
+
def categorize(query)
|
9
|
+
if query[:name] == "SCHEMA"
|
10
|
+
[:schema_queries]
|
11
|
+
elsif query[:name] == "TRANSACTION" || query[:sql] =~ /^begin/i || query[:sql] =~ /^commit/i
|
12
|
+
[:transaction_queries]
|
13
|
+
elsif query[:name] =~ /Create$/
|
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]
|
23
|
+
else
|
24
|
+
[:queries]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -4,10 +4,16 @@ 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
|
10
14
|
|
11
15
|
require_relative 'expectations/errors'
|
16
|
+
require_relative 'expectations/query_inspector'
|
12
17
|
require_relative 'expectations/collector'
|
13
|
-
require_relative 'expectations/matchers/
|
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.0
|
3
|
+
spec.version = '2.1.0'
|
4
4
|
spec.authors = ["Joseph Mastey"]
|
5
5
|
spec.email = ["hello@joemastey.com"]
|
6
6
|
|
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.description = %q{Adds new matchers to rspec to help you test whether your code is executing an unreasonable number of queries.}
|
9
9
|
spec.homepage = "https://github.com/jmmastey/rspec-activerecord-expectations"
|
10
10
|
spec.license = "MIT"
|
11
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
11
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
12
12
|
|
13
13
|
spec.metadata["homepage_uri"] = spec.homepage
|
14
14
|
spec.metadata["source_code_uri"] = spec.homepage
|
@@ -23,7 +23,12 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.require_paths = ["lib"]
|
24
24
|
|
25
25
|
spec.add_dependency "activerecord", ">= 5.0.0", "< 7.1.0"
|
26
|
-
|
26
|
+
|
27
|
+
if RUBY_PLATFORM == 'java'
|
28
|
+
spec.add_development_dependency "activerecord-jdbcsqlite3-adapter", '>= 60'
|
29
|
+
else
|
30
|
+
spec.add_development_dependency "sqlite3", "~> 1.0"
|
31
|
+
end
|
27
32
|
|
28
33
|
spec.add_development_dependency "pry", "~> 0.0"
|
29
34
|
spec.add_development_dependency "appraisal", "~> 2"
|
metadata
CHANGED
@@ -1,17 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-activerecord-expectations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 2.1.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-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name: activerecord
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
16
15
|
requirements:
|
17
16
|
- - ">="
|
@@ -20,8 +19,9 @@ dependencies:
|
|
20
19
|
- - "<"
|
21
20
|
- !ruby/object:Gem::Version
|
22
21
|
version: 7.1.0
|
23
|
-
|
22
|
+
name: activerecord
|
24
23
|
prerelease: false
|
24
|
+
type: :runtime
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
@@ -31,42 +31,42 @@ dependencies:
|
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: 7.1.0
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
|
-
name: sqlite3
|
35
34
|
requirement: !ruby/object:Gem::Requirement
|
36
35
|
requirements:
|
37
|
-
- - "
|
36
|
+
- - ">="
|
38
37
|
- !ruby/object:Gem::Version
|
39
|
-
version: '
|
40
|
-
|
38
|
+
version: '60'
|
39
|
+
name: activerecord-jdbcsqlite3-adapter
|
41
40
|
prerelease: false
|
41
|
+
type: :development
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
|
-
- - "
|
44
|
+
- - ">="
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: '
|
46
|
+
version: '60'
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
|
-
name: pry
|
49
48
|
requirement: !ruby/object:Gem::Requirement
|
50
49
|
requirements:
|
51
50
|
- - "~>"
|
52
51
|
- !ruby/object:Gem::Version
|
53
52
|
version: '0.0'
|
54
|
-
|
53
|
+
name: pry
|
55
54
|
prerelease: false
|
55
|
+
type: :development
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
58
|
- - "~>"
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: '0.0'
|
61
61
|
- !ruby/object:Gem::Dependency
|
62
|
-
name: appraisal
|
63
62
|
requirement: !ruby/object:Gem::Requirement
|
64
63
|
requirements:
|
65
64
|
- - "~>"
|
66
65
|
- !ruby/object:Gem::Version
|
67
66
|
version: '2'
|
68
|
-
|
67
|
+
name: appraisal
|
69
68
|
prerelease: false
|
69
|
+
type: :development
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
72
|
- - "~>"
|
@@ -102,7 +102,9 @@ 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
|
107
|
+
- lib/rspec/activerecord/expectations/query_inspector.rb
|
106
108
|
- rspec-activerecord-expectations.gemspec
|
107
109
|
homepage: https://github.com/jmmastey/rspec-activerecord-expectations
|
108
110
|
licenses:
|
@@ -119,14 +121,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
119
121
|
requirements:
|
120
122
|
- - ">="
|
121
123
|
- !ruby/object:Gem::Version
|
122
|
-
version: 2.
|
124
|
+
version: 2.5.0
|
123
125
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
126
|
requirements:
|
125
127
|
- - ">="
|
126
128
|
- !ruby/object:Gem::Version
|
127
129
|
version: '0'
|
128
130
|
requirements: []
|
129
|
-
rubygems_version: 3.
|
131
|
+
rubygems_version: 3.2.29
|
130
132
|
signing_key:
|
131
133
|
specification_version: 4
|
132
134
|
summary: A gem to test how many activerecord queries your code executes.
|
@@ -1,57 +0,0 @@
|
|
1
|
-
module RSpec::ActiveRecord::Expectations
|
2
|
-
module Matchers
|
3
|
-
class QueryCountMatcher
|
4
|
-
attr_reader :failure_message, :failure_message_when_negated
|
5
|
-
|
6
|
-
def initialize
|
7
|
-
@collector = Collector.new
|
8
|
-
|
9
|
-
@match_method = nil
|
10
|
-
@comparison = nil
|
11
|
-
@query_type = nil
|
12
|
-
end
|
13
|
-
|
14
|
-
def supports_block_expectations?
|
15
|
-
true
|
16
|
-
end
|
17
|
-
|
18
|
-
def matches?(block)
|
19
|
-
raise NoComparisonError unless @match_method
|
20
|
-
raise NoQueryTypeError unless @collector.valid_type?(@query_type)
|
21
|
-
|
22
|
-
result = block.call
|
23
|
-
|
24
|
-
!!@match_method.call
|
25
|
-
end
|
26
|
-
|
27
|
-
# COMPARISON TYPES
|
28
|
-
|
29
|
-
def fewer_than(n)
|
30
|
-
@comparison = n
|
31
|
-
@match_method = method(:match_fewer_than)
|
32
|
-
self
|
33
|
-
end
|
34
|
-
alias_method :less_than, :fewer_than
|
35
|
-
|
36
|
-
# TARGET QUERY TYPES
|
37
|
-
|
38
|
-
def queries
|
39
|
-
@query_type = :queries
|
40
|
-
self
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
# MATCHERS
|
46
|
-
|
47
|
-
def match_fewer_than
|
48
|
-
count = @collector.queries_of_type(@query_type)
|
49
|
-
|
50
|
-
@failure_message = "expected block to execute fewer than #{@comparison} queries, but it executed #{count}"
|
51
|
-
@failure_message_when_negated = "expected block not to execute fewer than #{@comparison} queries, but it executed #{count}"
|
52
|
-
|
53
|
-
count < @comparison
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|