rspec-activerecord-expectations 1.3.0 → 2.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 +4 -4
- data/.github/workflows/main.yml +38 -8
- data/CHANGELOG.md +28 -5
- data/README.md +98 -22
- data/lib/rspec/activerecord/expectations/matchers/query_count_matcher.rb +30 -68
- data/lib/rspec/activerecord/expectations/matchers/transaction_matcher.rb +99 -0
- data/lib/rspec/activerecord/expectations/message_builder.rb +160 -0
- data/lib/rspec/activerecord/expectations/query_inspector.rb +8 -3
- data/lib/rspec/activerecord/expectations.rb +18 -0
- data/rspec-activerecord-expectations.gemspec +8 -3
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f84a252fb5e5923d4bc6c03ed4d99ede76579017d8391dbf6f48a665236e54e
|
4
|
+
data.tar.gz: ce944950ee62940f67f79204966f5954fc28eac8f52dee79e18475933d9f3910
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4828d15b2420ae821199db596b96cc5d4630e3994b6c12bb2d6b95c32067388bb30161b318b0c03c49771d5b95e3010757e564c70a1f6840ab6934c14479f58d
|
7
|
+
data.tar.gz: d8220ae71a3e1dc6be694f976185262e019d55c6eedc1ec255492fbb5cd7a2988ac45e3519c14f1936cf8d0fd09062710f71b2d788322771e4ccde864be436a9
|
data/.github/workflows/main.yml
CHANGED
@@ -11,19 +11,19 @@ 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: ruby ${{ matrix.
|
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
29
|
- name: Install Gems w/ Rails ${{ matrix.rails }}
|
@@ -40,19 +40,19 @@ jobs:
|
|
40
40
|
test_legacy:
|
41
41
|
strategy:
|
42
42
|
matrix:
|
43
|
-
|
43
|
+
ruby-version: ['2.6', '2.5']
|
44
44
|
rails: ['5.0', '5.1', '5.2']
|
45
45
|
|
46
46
|
runs-on: ubuntu-latest
|
47
|
-
name:
|
47
|
+
name: ruby ${{ matrix.ruby-version }}, rails ${{ matrix.rails }}
|
48
48
|
|
49
49
|
steps:
|
50
50
|
- uses: actions/checkout@v2
|
51
51
|
|
52
|
-
- name: Set up Ruby ${{ matrix.
|
52
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
53
53
|
uses: ruby/setup-ruby@v1
|
54
54
|
with:
|
55
|
-
|
55
|
+
ruby-version: ${{ matrix.ruby-version }}
|
56
56
|
bundler-cache: true
|
57
57
|
|
58
58
|
- name: Install Gems w/ Rails ${{ matrix.rails }}
|
@@ -65,3 +65,33 @@ jobs:
|
|
65
65
|
|
66
66
|
- name: Run tests
|
67
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 }}
|
77
|
+
|
78
|
+
steps:
|
79
|
+
- uses: actions/checkout@v2
|
80
|
+
|
81
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
82
|
+
uses: ruby/setup-ruby@v1
|
83
|
+
with:
|
84
|
+
ruby-version: ${{ matrix.ruby-version }}
|
85
|
+
bundler-cache: true
|
86
|
+
|
87
|
+
- name: Install Gems w/ Rails ${{ matrix.rails }}
|
88
|
+
env:
|
89
|
+
MATRIX_RAILS_VERSION: ${{ matrix.rails }}
|
90
|
+
run: |
|
91
|
+
export BUNDLE_GEMFILE="${GITHUB_WORKSPACE}/gemfiles/rails_${MATRIX_RAILS_VERSION}.gemfile"
|
92
|
+
gem install bundler
|
93
|
+
bundle install --jobs 4 --retry 3
|
94
|
+
|
95
|
+
- name: Run tests
|
96
|
+
run: bundle exec rspec
|
97
|
+
|
data/CHANGELOG.md
CHANGED
@@ -1,25 +1,48 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
## [
|
3
|
+
## [2.3.0] - 2022-01-29
|
4
|
+
- Add quantifiers to transaction matchers
|
5
|
+
- Add much more complicated English output for readable matchers
|
6
|
+
- Update README for the same
|
7
|
+
|
8
|
+
## [2.2.0] - 2022-01-14
|
9
|
+
- Adds transaction matcher to verify that code was executed within a
|
10
|
+
transaction at minimum
|
11
|
+
- Transaction matcher also allows for commits and rollbacks
|
12
|
+
- Update README for accompanying functionality
|
13
|
+
|
14
|
+
## [2.1.1] - 2022-01-12
|
15
|
+
- Gemspec is really generated using the version of ruby that's locally in use.
|
16
|
+
Update that build artifact to use non-java dependencies.
|
17
|
+
|
18
|
+
## [2.1.0] - 2022-01-12
|
19
|
+
- Change CI matrix for more version compatibility
|
20
|
+
- Update gemspec to move some deps into development only
|
21
|
+
- README changes
|
22
|
+
|
23
|
+
## [2.0.0] - 2022-01-07
|
24
|
+
- Require much more recent ruby, to use syntax that's been around since 2017
|
25
|
+
|
26
|
+
## [1.3.0] - 2021-12-31
|
4
27
|
- Add `repeatedly_load` matcher
|
5
28
|
- Add query type matchers for `load_queries`, `schema_queries`, `transaction_queries`, `destroy_queries`
|
6
29
|
- Allow singular version of all query types (e.g. `transaction_queries` vs `transaction_query`)
|
7
30
|
- Fix failure message for `execute.exactly` matcher
|
8
31
|
|
9
|
-
## [1.2.0] -
|
32
|
+
## [1.2.0] - 2021-12-31
|
10
33
|
- Add `query` as a synonym for `queries`
|
11
34
|
- Ignore schema and transaction queries in query count
|
12
35
|
- Add beginning of recording specific query types
|
13
36
|
- Add query count matcher for `exactly`
|
14
37
|
|
15
|
-
## [1.1.0] -
|
38
|
+
## [1.1.0] - 2021-12-30
|
16
39
|
- Add query count matchers for e.g. `less_than_or_equal_to`, `greater_than`
|
17
40
|
|
18
|
-
## [1.0.1] -
|
41
|
+
## [1.0.1] - 2021-12-30
|
19
42
|
- Pin all the dependencies to a proper working subset
|
20
43
|
- Expand testing to many rails / ruby combinations
|
21
44
|
|
22
|
-
## [1.0.0] -
|
45
|
+
## [1.0.0] - 2021-12-30
|
23
46
|
- Basic gem w/ all the fixins and README and such
|
24
47
|
- Add `less_than` comparison, and `queries` type
|
25
48
|
- 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|
|
@@ -109,18 +110,34 @@ This matcher will track ActiveRecord's built in load methods to prevent those
|
|
109
110
|
N+1 situations. Using eager loading (e.g. `Track.all.includes(:album)`) will
|
110
111
|
allow these expectations to pass as expected!
|
111
112
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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.
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
tracks = Track.all
|
126
|
+
|
127
|
+
expect {
|
128
|
+
TrackSerializer.perform(tracks, batch_size: tracks.count)
|
129
|
+
}.not_to repeatedly_load('Track')
|
130
|
+
```
|
117
131
|
|
118
132
|
## Counting Queries
|
119
133
|
|
120
|
-
|
134
|
+
Some services won't necessarily have N+1 issues with records loading, but might
|
135
|
+
still have problems with executing too many queries. In this case, the
|
136
|
+
`repeatedly_load` matcher might be insufficient.
|
121
137
|
|
122
|
-
|
123
|
-
|
138
|
+
In this case, consider creating an expectation on the total number of queries
|
139
|
+
executed by your code. Several comparison types are available, along with some
|
140
|
+
aliases to allow for easier to read tests.
|
124
141
|
|
125
142
|
```ruby
|
126
143
|
expect {}.to execute.less_than(20).queries
|
@@ -143,15 +160,10 @@ expect {}.to execute.at_least(1).query # singular form also accepted
|
|
143
160
|
|
144
161
|
### Specific Query Types
|
145
162
|
|
146
|
-
You can
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
**Note:** Transaction (for example, `ROLLBACK`) queries are not counted in any of these
|
151
|
-
categories, nor are queries that load the DB schema.
|
152
|
-
|
153
|
-
**Note:** Destroy and delete queries are both condensed into the matcher for
|
154
|
-
`destroy_queries`.
|
163
|
+
You can make assertions for the total number of queries executed, but sometimes
|
164
|
+
it's more valuable to assert that a particular _type_ of query was executed.
|
165
|
+
For example, a particular number of queries to destroy records. There are
|
166
|
+
matchers available for that purpose as well!
|
155
167
|
|
156
168
|
```ruby
|
157
169
|
expect {}.to execute.exactly(20).queries
|
@@ -165,6 +177,74 @@ expect {}.to execute.exactly(20).schema_queries
|
|
165
177
|
expect {}.to execute.exactly(20).transaction_queries
|
166
178
|
```
|
167
179
|
|
180
|
+
**Note:** Transaction (for example, `ROLLBACK`) queries are not counted in any of these
|
181
|
+
categories, nor are queries that load the DB schema.
|
182
|
+
|
183
|
+
**Note:** Destroy and delete queries are both condensed into the matcher for
|
184
|
+
the errorthe error `destroy_queries`.
|
185
|
+
|
186
|
+
## Transaction Management
|
187
|
+
|
188
|
+
Sometimes, it makes sense to monitor whether database transactions were
|
189
|
+
successful or not. This is very similar to using `expect{}.to change(SomeModel,
|
190
|
+
:count)` in a spec, but nonetheless it can be useful to assert transactions
|
191
|
+
themselves. Some assertions are available for this purpose.
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
expect {}.to execute_a_transaction
|
195
|
+
expect {}.to rollback_a_transaction
|
196
|
+
expect {}.to roll_back_a_transaction
|
197
|
+
expect {}.to commit_a_transaction
|
198
|
+
```
|
199
|
+
|
200
|
+
A complication to this scheme is that Rails tries not to make unnecessary database
|
201
|
+
calls, which means that attempting to save a model that has failing validations
|
202
|
+
won't actually attempt to save to the database.
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
expect {
|
206
|
+
MyClass.create!(required_field: nil)
|
207
|
+
}.to rollback_a_transaction
|
208
|
+
```
|
209
|
+
|
210
|
+
This assertion will fail, as `create!` will never make it as far as the
|
211
|
+
database. That said, if you manually create a transaction, _and you select
|
212
|
+
data within that transaction_, you may assert a rollback.
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
expect {
|
216
|
+
MyClass.first # triggers the transaction
|
217
|
+
MyClass.create!(required_field: nil)
|
218
|
+
}.to rollback_a_transaction
|
219
|
+
```
|
220
|
+
|
221
|
+
It you need to make transaction-related assertions of this sort, your best bet
|
222
|
+
may be to assert that a commit statement was _not_ issued.
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
expect do
|
226
|
+
MyClass.create!(required_field: nil)
|
227
|
+
rescue
|
228
|
+
# NOOP
|
229
|
+
end.not_to rollback_a_transaction
|
230
|
+
```
|
231
|
+
|
232
|
+
Note that ActiveRecord will not only roll back the transaction, but also
|
233
|
+
re-raise errors. As such, it's necessary in this example to rescue that error,
|
234
|
+
otherwise the test would fail simply because the code caused a `raise`.
|
235
|
+
|
236
|
+
### Counting Transactions
|
237
|
+
|
238
|
+
Similar to counting queries, you can quantify the number of transactions you
|
239
|
+
expect to succeed / fail. This is probably of limited value in all but some
|
240
|
+
very specific cases.
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
expect {}.to commit_a_transaction.once
|
244
|
+
expect {}.to rollback_a_transaction.exactly(5).times
|
245
|
+
expect {}.to commit_a_transaction.at_least(5).times
|
246
|
+
```
|
247
|
+
|
168
248
|
## Future Planned Functionality
|
169
249
|
|
170
250
|
This gem still has lots of future functionality. See below.
|
@@ -176,10 +256,6 @@ expect {}.to execute.at_least(2).load_queries("Audited::Audit")
|
|
176
256
|
expect {}.to execute.at_least(2).activerecord_queries
|
177
257
|
expect {}.to execute.at_least(2).hand_rolled_queries
|
178
258
|
|
179
|
-
expect {}.not_to rollback_transaction.exactly(5).times
|
180
|
-
expect {}.not_to commit_transaction.once
|
181
|
-
expect {}.to run_a_transaction
|
182
|
-
|
183
259
|
expect {}.to create.exactly(5).of_type(User)
|
184
260
|
expect {}.to insert.exactly(5).subscription_changes
|
185
261
|
expect {}.to update.exactly(2).of_any_type
|
@@ -1,16 +1,25 @@
|
|
1
1
|
module RSpec::ActiveRecord::Expectations
|
2
2
|
module Matchers
|
3
3
|
class QueryCountMatcher
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :collector, :quantifier, :comparison, :query_type
|
5
5
|
|
6
6
|
def initialize
|
7
7
|
@collector = Collector.new
|
8
|
+
@message_builder = MessageBuilder.new(self)
|
8
9
|
|
9
10
|
@match_method = nil
|
10
|
-
@
|
11
|
+
@quantifier = nil
|
11
12
|
@query_type = nil
|
12
13
|
end
|
13
14
|
|
15
|
+
def failure_message
|
16
|
+
@message_builder.failure_message
|
17
|
+
end
|
18
|
+
|
19
|
+
def failure_message_when_negated
|
20
|
+
@message_builder.failure_message_when_negated
|
21
|
+
end
|
22
|
+
|
14
23
|
def supports_block_expectations?
|
15
24
|
true
|
16
25
|
end
|
@@ -26,39 +35,44 @@ module RSpec::ActiveRecord::Expectations
|
|
26
35
|
result
|
27
36
|
end
|
28
37
|
|
29
|
-
#
|
38
|
+
# QUANTIFIERS
|
30
39
|
|
31
40
|
def less_than(n)
|
32
|
-
@
|
33
|
-
@
|
41
|
+
@quantifier = n
|
42
|
+
@comparison = :less_than
|
43
|
+
@match_method = -> { actual_count < @quantifier }
|
34
44
|
self
|
35
45
|
end
|
36
46
|
alias_method :fewer_than, :less_than
|
37
47
|
|
38
48
|
def less_than_or_equal_to(n)
|
39
|
-
@
|
40
|
-
@
|
49
|
+
@quantifier = n
|
50
|
+
@comparison = :less_than_or_equal_to
|
51
|
+
@match_method = -> { actual_count <= @quantifier }
|
41
52
|
self
|
42
53
|
end
|
43
54
|
alias_method :at_most, :less_than_or_equal_to
|
44
55
|
|
45
56
|
def greater_than(n)
|
46
|
-
@
|
47
|
-
@
|
57
|
+
@quantifier = n
|
58
|
+
@comparison = :greater_than
|
59
|
+
@match_method = -> { actual_count > @quantifier }
|
48
60
|
self
|
49
61
|
end
|
50
62
|
alias_method :more_than, :greater_than
|
51
63
|
|
52
64
|
def greater_than_or_equal_to(n)
|
53
|
-
@
|
54
|
-
@
|
65
|
+
@quantifier = n
|
66
|
+
@comparison = :greater_than_or_equal_to
|
67
|
+
@match_method = -> { actual_count >= @quantifier }
|
55
68
|
self
|
56
69
|
end
|
57
70
|
alias_method :at_least, :greater_than_or_equal_to
|
58
71
|
|
59
72
|
def exactly(n)
|
60
|
-
@
|
61
|
-
@
|
73
|
+
@quantifier = n
|
74
|
+
@comparison = :exactly
|
75
|
+
@match_method = -> { actual_count == @quantifier }
|
62
76
|
self
|
63
77
|
end
|
64
78
|
|
@@ -79,62 +93,10 @@ module RSpec::ActiveRecord::Expectations
|
|
79
93
|
end
|
80
94
|
end
|
81
95
|
|
82
|
-
#
|
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}"
|
96
|
+
# helper for message builder
|
136
97
|
|
137
|
-
|
98
|
+
def actual_count
|
99
|
+
@collector.queries_of_type(@query_type)
|
138
100
|
end
|
139
101
|
end
|
140
102
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module RSpec::ActiveRecord::Expectations
|
2
|
+
module Matchers
|
3
|
+
class TransactionMatcher
|
4
|
+
attr_reader :collector, :quantifier, :comparison, :query_type
|
5
|
+
|
6
|
+
def initialize(transaction_type)
|
7
|
+
@collector = Collector.new
|
8
|
+
@query_type = transaction_type
|
9
|
+
@message_builder = MessageBuilder.new(self)
|
10
|
+
|
11
|
+
self.at_least(1)
|
12
|
+
end
|
13
|
+
|
14
|
+
def failure_message
|
15
|
+
@message_builder.failure_message
|
16
|
+
end
|
17
|
+
|
18
|
+
def failure_message_when_negated
|
19
|
+
@message_builder.failure_message_when_negated
|
20
|
+
end
|
21
|
+
|
22
|
+
def supports_block_expectations?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def matches?(block)
|
27
|
+
raise NoComparisonError unless @match_method
|
28
|
+
|
29
|
+
block.call
|
30
|
+
result = @match_method.call
|
31
|
+
@collector.finalize
|
32
|
+
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
# QUANTIFIERS
|
37
|
+
|
38
|
+
def less_than(n)
|
39
|
+
@quantifier = n
|
40
|
+
@comparison = :less_than
|
41
|
+
@match_method = -> { actual_count < @quantifier }
|
42
|
+
self
|
43
|
+
end
|
44
|
+
alias_method :fewer_than, :less_than
|
45
|
+
|
46
|
+
def less_than_or_equal_to(n)
|
47
|
+
@quantifier = n
|
48
|
+
@comparison = :less_than_or_equal_to
|
49
|
+
@match_method = -> { actual_count <= @quantifier }
|
50
|
+
self
|
51
|
+
end
|
52
|
+
alias_method :at_most, :less_than_or_equal_to
|
53
|
+
|
54
|
+
def greater_than(n)
|
55
|
+
@quantifier = n
|
56
|
+
@comparison = :greater_than
|
57
|
+
@match_method = -> { actual_count > @quantifier }
|
58
|
+
self
|
59
|
+
end
|
60
|
+
alias_method :more_than, :greater_than
|
61
|
+
|
62
|
+
def greater_than_or_equal_to(n)
|
63
|
+
@quantifier = n
|
64
|
+
@comparison = :greater_than_or_equal_to
|
65
|
+
@match_method = -> { actual_count >= @quantifier }
|
66
|
+
self
|
67
|
+
end
|
68
|
+
alias_method :at_least, :greater_than_or_equal_to
|
69
|
+
|
70
|
+
def exactly(n)
|
71
|
+
@quantifier = n
|
72
|
+
@comparison = :exactly
|
73
|
+
@match_method = -> { actual_count == @quantifier }
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
def once
|
78
|
+
exactly(1).time
|
79
|
+
end
|
80
|
+
|
81
|
+
def twice
|
82
|
+
exactly(2).times
|
83
|
+
end
|
84
|
+
|
85
|
+
def thrice # hehe
|
86
|
+
exactly(3).times
|
87
|
+
end
|
88
|
+
|
89
|
+
def times
|
90
|
+
self # NOOP
|
91
|
+
end
|
92
|
+
alias_method :time, :times
|
93
|
+
|
94
|
+
def actual_count
|
95
|
+
@collector.queries_of_type(@query_type)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module RSpec::ActiveRecord::Expectations
|
2
|
+
class MessageBuilder
|
3
|
+
attr_reader :matcher, :phrase_builder
|
4
|
+
|
5
|
+
def initialize(matcher)
|
6
|
+
@matcher = matcher
|
7
|
+
@phrase_builder = case matcher
|
8
|
+
when Matchers::QueryCountMatcher then QueryPhrases.new(matcher)
|
9
|
+
when Matchers::TransactionMatcher then TransactionPhrases.new(matcher)
|
10
|
+
else raise ArgumentError
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def failure_message
|
15
|
+
"expected block to #{phrase_builder.prefix}, but it #{phrase_builder.suffix}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def failure_message_when_negated
|
19
|
+
"expected block not to #{phrase_builder.prefix}, but it #{phrase_builder.negative_suffix}"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# TODO: expect {}.not_to execute.less_than_or_equal_to(3).insert_queries
|
25
|
+
# expected block not to execute at most 3 insert queries, but it executed 0
|
26
|
+
|
27
|
+
|
28
|
+
class QueryPhrases
|
29
|
+
attr_reader :matcher
|
30
|
+
|
31
|
+
def initialize(matcher)
|
32
|
+
@matcher = matcher
|
33
|
+
end
|
34
|
+
|
35
|
+
def prefix
|
36
|
+
"execute #{comparison_phrase} #{query_type_name}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def suffix
|
40
|
+
if matcher.actual_count == 0
|
41
|
+
"didn't execute any"
|
42
|
+
elsif matcher.actual_count == 1
|
43
|
+
"executed one"
|
44
|
+
else
|
45
|
+
"executed #{matcher.actual_count}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# using positive suffix from above can cause double negatives
|
50
|
+
def negative_suffix
|
51
|
+
if matcher.comparison == :exactly
|
52
|
+
"did so"
|
53
|
+
elsif matcher.actual_count == 1
|
54
|
+
"executed one"
|
55
|
+
else
|
56
|
+
"executed #{matcher.actual_count}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def query_type_name
|
63
|
+
if matcher.quantifier == 1
|
64
|
+
matcher.query_type.to_s.gsub("_", " ").gsub("queries", "query")
|
65
|
+
else
|
66
|
+
matcher.query_type.to_s.gsub("_", " ")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def comparison_phrase
|
71
|
+
quant = if matcher.quantifier == 1 && matcher.comparison == :exactly
|
72
|
+
"a"
|
73
|
+
elsif matcher.quantifier == 1
|
74
|
+
"one"
|
75
|
+
else
|
76
|
+
matcher.quantifier
|
77
|
+
end
|
78
|
+
|
79
|
+
case matcher.comparison
|
80
|
+
when :exactly then quant
|
81
|
+
when :greater_than then "more than #{quant}"
|
82
|
+
when :greater_than_or_equal_to then "at least #{quant}"
|
83
|
+
when :less_than then "less than #{quant}"
|
84
|
+
when :less_than_or_equal_to then "at most #{quant}"
|
85
|
+
else raise ArgumentError, "unsupported comparison matcher #{matcher.comparison}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class TransactionPhrases
|
91
|
+
attr_reader :matcher
|
92
|
+
|
93
|
+
def initialize(matcher)
|
94
|
+
@matcher = matcher
|
95
|
+
end
|
96
|
+
|
97
|
+
def prefix
|
98
|
+
nouns = matcher.quantifier == 1 ? "transaction" : "transactions"
|
99
|
+
verb = case matcher.query_type
|
100
|
+
when :transaction_queries then "execute"
|
101
|
+
when :rollback_queries then "roll back"
|
102
|
+
when :commit_queries then "commit"
|
103
|
+
end
|
104
|
+
|
105
|
+
"#{verb} #{comparison} #{nouns}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def suffix
|
109
|
+
singular_verb = case matcher.query_type
|
110
|
+
when :transaction_queries then "execute"
|
111
|
+
when :rollback_queries then "roll back"
|
112
|
+
when :commit_queries then "commit"
|
113
|
+
end
|
114
|
+
|
115
|
+
plural_verb = case matcher.query_type
|
116
|
+
when :transaction_queries then "executed"
|
117
|
+
when :rollback_queries then "rolled back"
|
118
|
+
when :commit_queries then "committed"
|
119
|
+
end
|
120
|
+
|
121
|
+
if matcher.actual_count == 0
|
122
|
+
"didn't #{singular_verb} any"
|
123
|
+
elsif matcher.actual_count == 1
|
124
|
+
"#{plural_verb} one"
|
125
|
+
else
|
126
|
+
"#{plural_verb} #{matcher.actual_count}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def negative_suffix
|
131
|
+
if matcher.comparison == :exactly
|
132
|
+
"did so"
|
133
|
+
else
|
134
|
+
suffix
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def comparison
|
141
|
+
quant = if matcher.quantifier == 1 && matcher.comparison == :exactly
|
142
|
+
"a"
|
143
|
+
elsif matcher.quantifier == 1
|
144
|
+
"one"
|
145
|
+
else
|
146
|
+
matcher.quantifier
|
147
|
+
end
|
148
|
+
|
149
|
+
case matcher.comparison
|
150
|
+
when :exactly then quant
|
151
|
+
when :greater_than then "more than #{quant}"
|
152
|
+
when :greater_than_or_equal_to then "at least #{quant}"
|
153
|
+
when :less_than then "less than #{quant}"
|
154
|
+
when :less_than_or_equal_to then "at most #{quant}"
|
155
|
+
else raise ArgumentError, "unsupported comparison matcher #{matcher.comparison}"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -1,13 +1,18 @@
|
|
1
1
|
module RSpec::ActiveRecord::Expectations
|
2
2
|
class QueryInspector
|
3
3
|
def self.valid_query_types
|
4
|
-
[:queries, :schema_queries, :
|
5
|
-
:
|
4
|
+
[:queries, :schema_queries, :insert_queries, :load_queries,
|
5
|
+
:destroy_queries, :exists_queries,
|
6
|
+
:transaction_queries, :commit_queries, :rollback_queries]
|
6
7
|
end
|
7
8
|
|
8
9
|
def categorize(query)
|
9
10
|
if query[:name] == "SCHEMA"
|
10
11
|
[:schema_queries]
|
12
|
+
elsif query[:sql] =~ /^commit/i
|
13
|
+
[:commit_queries]
|
14
|
+
elsif query[:sql] =~ /^rollback/i
|
15
|
+
[:rollback_queries]
|
11
16
|
elsif query[:name] == "TRANSACTION"
|
12
17
|
[:transaction_queries]
|
13
18
|
elsif query[:name] =~ /Create$/
|
@@ -18,7 +23,7 @@ module RSpec::ActiveRecord::Expectations
|
|
18
23
|
[:queries, :destroy_queries]
|
19
24
|
elsif query[:name] =~ /Delete All$/
|
20
25
|
[:queries, :destroy_queries]
|
21
|
-
elsif query[:name] =~ /Exists
|
26
|
+
elsif query[:name] =~ /Exists\??$/
|
22
27
|
[:queries, :exists_queries]
|
23
28
|
else
|
24
29
|
[:queries]
|
@@ -8,6 +8,22 @@ module RSpec
|
|
8
8
|
def repeatedly_load(klass)
|
9
9
|
Matchers::LoadMatcher.new(klass)
|
10
10
|
end
|
11
|
+
|
12
|
+
def execute_a_transaction
|
13
|
+
Matchers::TransactionMatcher.new(:transaction_queries)
|
14
|
+
end
|
15
|
+
|
16
|
+
def rollback_a_transaction
|
17
|
+
Matchers::TransactionMatcher.new(:rollback_queries)
|
18
|
+
end
|
19
|
+
|
20
|
+
def roll_back_a_transaction
|
21
|
+
Matchers::TransactionMatcher.new(:rollback_queries)
|
22
|
+
end
|
23
|
+
|
24
|
+
def commit_a_transaction
|
25
|
+
Matchers::TransactionMatcher.new(:commit_queries)
|
26
|
+
end
|
11
27
|
end
|
12
28
|
end
|
13
29
|
end
|
@@ -15,5 +31,7 @@ end
|
|
15
31
|
require_relative 'expectations/errors'
|
16
32
|
require_relative 'expectations/query_inspector'
|
17
33
|
require_relative 'expectations/collector'
|
34
|
+
require_relative 'expectations/message_builder'
|
18
35
|
require_relative 'expectations/matchers/query_count_matcher'
|
19
36
|
require_relative 'expectations/matchers/load_matcher'
|
37
|
+
require_relative 'expectations/matchers/transaction_matcher'
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |spec|
|
2
2
|
spec.name = "rspec-activerecord-expectations"
|
3
|
-
spec.version = '
|
3
|
+
spec.version = '2.3.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,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-activerecord-expectations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.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: 2022-01-
|
11
|
+
date: 2022-01-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -37,7 +37,7 @@ dependencies:
|
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: '1.0'
|
40
|
-
type: :
|
40
|
+
type: :development
|
41
41
|
prerelease: false
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
@@ -104,6 +104,8 @@ files:
|
|
104
104
|
- lib/rspec/activerecord/expectations/errors.rb
|
105
105
|
- lib/rspec/activerecord/expectations/matchers/load_matcher.rb
|
106
106
|
- lib/rspec/activerecord/expectations/matchers/query_count_matcher.rb
|
107
|
+
- lib/rspec/activerecord/expectations/matchers/transaction_matcher.rb
|
108
|
+
- lib/rspec/activerecord/expectations/message_builder.rb
|
107
109
|
- lib/rspec/activerecord/expectations/query_inspector.rb
|
108
110
|
- rspec-activerecord-expectations.gemspec
|
109
111
|
homepage: https://github.com/jmmastey/rspec-activerecord-expectations
|
@@ -121,7 +123,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
121
123
|
requirements:
|
122
124
|
- - ">="
|
123
125
|
- !ruby/object:Gem::Version
|
124
|
-
version: 2.
|
126
|
+
version: 2.5.0
|
125
127
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
128
|
requirements:
|
127
129
|
- - ">="
|