active_record_change_matchers 1.0.0 → 1.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 +33 -0
- data/CHANGELOG.md +8 -0
- data/README.md +276 -99
- data/Rakefile +0 -6
- data/active_record_change_matchers.gemspec +21 -22
- data/db/migrate/20150225014908_create_people.rb +1 -1
- data/db/migrate/20151017231107_create_dogs.rb +1 -1
- data/db/migrate/20160101000000_create_pets.rb +9 -0
- data/db/schema.rb +23 -15
- data/lib/active_record_change_matchers/hash_format.rb +9 -0
- data/lib/active_record_change_matchers/matchers/create_a_new_matcher.rb +142 -0
- data/lib/active_record_change_matchers/matchers/create_associated_matcher.rb +242 -0
- data/lib/active_record_change_matchers/matchers/create_records_matcher.rb +191 -0
- data/lib/active_record_change_matchers/strategies/timestamp_strategy.rb +1 -1
- data/lib/active_record_change_matchers/version.rb +1 -1
- data/lib/active_record_change_matchers.rb +1 -0
- metadata +45 -46
- data/active_record_block_matchers.gemspec +0 -33
- data/lib/active_record_block_matchers.rb +0 -7
- data/lib/active_record_change_matchers/create_a_new_matcher.rb +0 -94
- data/lib/active_record_change_matchers/create_records_matcher.rb +0 -112
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff783e18ce73bf25fcd53447eefa3062d2b0e5994d20c9766e980522d7adf663
|
|
4
|
+
data.tar.gz: c34bcc770bdfe89d58cbe11f0f14e2659d55cfa908ba56f624dafba3efb2c5ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 351cf1793f1682f8b3b38e7c4768e34f93622a2c71e9bf116f817692ae7031da0456af12aadaf1aacf9f6ccb68e5d0999ed6254ff38dd51f8f2100976331bf1d
|
|
7
|
+
data.tar.gz: e2df1ac06efb41a1bcdd1310f681afbf526dd7feb4353d176f610fc93501938dae34451fcf523a93fcd76be397e2be4ac58f1e7339da841bfc07b6c7bcf9309a
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Unit tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ "master" ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ "master" ]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
strategy:
|
|
20
|
+
matrix:
|
|
21
|
+
ruby-version: ['3.3', '3.4', '4.0']
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- name: Set up Ruby
|
|
26
|
+
uses: ruby/setup-ruby@v1
|
|
27
|
+
with:
|
|
28
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
29
|
+
bundler-cache: true
|
|
30
|
+
- name: Prepare test database
|
|
31
|
+
run: bundle exec rake db:setup
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: bundle exec rspec
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Master (Unreleased)
|
|
4
4
|
|
|
5
|
+
## 1.1.0 (2026-01-27)
|
|
6
|
+
- Add `create_associated(scope)` matcher: verifies creation of new record(s) within an association scope (e.g. `record.items`). Supports `with_attributes`, `which`, `and_return_it`, and `and_return_them`. Model class is inferred from the scope.
|
|
7
|
+
- Add `and_return_it` / `and_return_them` modifiers.
|
|
8
|
+
- Drop support for Rails < 7
|
|
9
|
+
|
|
10
|
+
## 1.0.1 (2024-07-03)
|
|
11
|
+
- Fix broken timestamp stategy after adding the frozen time support.
|
|
12
|
+
|
|
5
13
|
## 1.0.0 (2024-07-03)
|
|
6
14
|
|
|
7
15
|
Hard fork. Changes since the original version:
|
data/README.md
CHANGED
|
@@ -1,73 +1,113 @@
|
|
|
1
1
|
# active_record_change_matchers
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
RSpec matchers that assert **exactly which** ActiveRecord records were created inside a block—by count, model, attributes, and association scope.
|
|
4
|
+
|
|
5
|
+
This is a hard fork of [active_record_block_matchers](https://github.com/nwallace/active_record_block_matchers). See [CHANGELOG](CHANGELOG.md) for changes since the original.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of contents
|
|
10
|
+
|
|
11
|
+
- [Requirements](#requirements)
|
|
12
|
+
- [Installation](#installation)
|
|
13
|
+
- [Quick start](#quick-start)
|
|
14
|
+
- [Matcher reference](#matcher-reference)
|
|
15
|
+
- [Summary](#matcher-summary)
|
|
16
|
+
- [create_a / create_a_new](#create_a--create_a_new)
|
|
17
|
+
- [create / create_records](#create--create_records)
|
|
18
|
+
- [create_associated](#create_associated)
|
|
19
|
+
- [Record retrieval strategies](#record-retrieval-strategies)
|
|
20
|
+
- [Configuration](#configuration)
|
|
21
|
+
- [Tips and gotchas](#tips-and-gotchas)
|
|
22
|
+
- [Development](#development)
|
|
23
|
+
- [Contributing](#contributing)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- **Ruby** ≥ 3.3
|
|
30
|
+
- **Rails** ≥ 7 (ActiveRecord)
|
|
31
|
+
- **RSpec** ≥ 3 (rspec-expectations)
|
|
32
|
+
|
|
33
|
+
---
|
|
5
34
|
|
|
6
35
|
## Installation
|
|
7
36
|
|
|
8
|
-
Add
|
|
37
|
+
Add to your Gemfile:
|
|
9
38
|
|
|
10
39
|
```ruby
|
|
11
40
|
gem 'active_record_change_matchers'
|
|
12
41
|
```
|
|
13
42
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
$ bundle
|
|
43
|
+
Then:
|
|
17
44
|
|
|
18
|
-
|
|
45
|
+
```bash
|
|
46
|
+
bundle install
|
|
47
|
+
```
|
|
19
48
|
|
|
20
|
-
|
|
49
|
+
---
|
|
21
50
|
|
|
22
|
-
## Quick
|
|
51
|
+
## Quick start
|
|
23
52
|
|
|
24
53
|
```ruby
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.with_attributes(
|
|
29
|
-
.which {|bob| expect(AuthLibrary.authenticate("bob", "BlueSteel45")).to eq bob }
|
|
54
|
+
# One record of a given model
|
|
55
|
+
expect { post :create, params: { user: { name: "Bob" } } }
|
|
56
|
+
.to create_a(User)
|
|
57
|
+
.with_attributes(name: "bob") # e.g. after downcasing in a callback
|
|
30
58
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
59
|
+
# Several models, exact counts
|
|
60
|
+
expect { sign_up!("bob", "secret") }
|
|
61
|
+
.to create(User => 1, Profile => 1)
|
|
34
62
|
.with_attributes(
|
|
35
|
-
User
|
|
36
|
-
Profile => [{avatar_url:
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
User => [{ name: "bob" }],
|
|
64
|
+
Profile => [{ avatar_url: Profile.default_avatar_url }]
|
|
65
|
+
)
|
|
66
|
+
.which { |records|
|
|
67
|
+
user = records[User].first
|
|
68
|
+
profile = records[Profile].first
|
|
69
|
+
expect(user.profile).to eq profile
|
|
41
70
|
}
|
|
71
|
+
|
|
72
|
+
# New records only within an association
|
|
73
|
+
expect { create_items }
|
|
74
|
+
.to create_associated(record.items => 2)
|
|
75
|
+
.with_attributes([{ content: "item1" }, { content: "item2" }])
|
|
42
76
|
```
|
|
43
77
|
|
|
44
|
-
|
|
78
|
+
---
|
|
45
79
|
|
|
46
|
-
|
|
80
|
+
## Matcher reference
|
|
47
81
|
|
|
48
|
-
|
|
82
|
+
### Matcher summary
|
|
49
83
|
|
|
50
|
-
|
|
84
|
+
| Matcher | Aliases | Use when you want to assert… |
|
|
85
|
+
|----------------------|-------------------|--------------------------------------------------------|
|
|
86
|
+
| `create_a(Model)` | `create_an`, `create_a_new` | Exactly one new record of that model |
|
|
87
|
+
| `create(Model => n, …)` | `create_records` | Exact counts of new records per model |
|
|
88
|
+
| `create_associated(scope)` | — | New records only within that association (e.g. `user.posts`) |
|
|
51
89
|
|
|
52
|
-
|
|
53
|
-
expect { User.create! }.to create_a(User)
|
|
54
|
-
```
|
|
90
|
+
All support chaining:
|
|
55
91
|
|
|
56
|
-
|
|
92
|
+
- **`.with_attributes(...)`** – attribute (or computed) expectations; works with [composable matchers](https://rspec.info/features/3-13/rspec-expectations/composing-matchers/).
|
|
93
|
+
- **`.which { |record_or_hash| ... }`** – extra expectations in a block (e.g. associations, side effects).
|
|
94
|
+
- **`.and_return_it`** / **`.and_return_them`** – that the block’s return value is the created record(s).
|
|
57
95
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
96
|
+
`create_a` also supports **`.which_is_expected_to(matcher)`** for a single composable matcher on the new record.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### create_a / create_a_new
|
|
101
|
+
|
|
102
|
+
Asserts the block creates **exactly one** new record of the given model. Ignores records that already existed before the block ran.
|
|
61
103
|
|
|
62
|
-
|
|
104
|
+
**Minimal:**
|
|
63
105
|
|
|
64
106
|
```ruby
|
|
65
|
-
expect { User.create!(
|
|
66
|
-
.to create_a(User)
|
|
67
|
-
.with_attributes(username: "bob")
|
|
107
|
+
expect { User.create! }.to create_a(User)
|
|
68
108
|
```
|
|
69
109
|
|
|
70
|
-
|
|
110
|
+
**With attributes** (DB columns or any reader, e.g. from callbacks):
|
|
71
111
|
|
|
72
112
|
```ruby
|
|
73
113
|
expect { User.create!(username: "BOB") }
|
|
@@ -75,7 +115,7 @@ expect { User.create!(username: "BOB") }
|
|
|
75
115
|
.with_attributes(username: "bob")
|
|
76
116
|
```
|
|
77
117
|
|
|
78
|
-
|
|
118
|
+
**With composable matchers:**
|
|
79
119
|
|
|
80
120
|
```ruby
|
|
81
121
|
expect { User.create!(username: "bob") }
|
|
@@ -83,133 +123,270 @@ expect { User.create!(username: "bob") }
|
|
|
83
123
|
.with_attributes(username: a_string_starting_with("b"))
|
|
84
124
|
```
|
|
85
125
|
|
|
86
|
-
|
|
126
|
+
**With a matcher (`.which_is_expected_to`):**
|
|
87
127
|
|
|
88
128
|
```ruby
|
|
89
|
-
expect { User.create!(username: "BOB", password: "
|
|
129
|
+
expect { User.create!(username: "BOB", password: "secret") }
|
|
90
130
|
.to create_a(User)
|
|
91
131
|
.which_is_expected_to(
|
|
92
132
|
have_attributes(encrypted_password: be_present)
|
|
93
|
-
|
|
133
|
+
.and(eq(Auth.authenticate("bob", "secret")))
|
|
94
134
|
)
|
|
95
135
|
```
|
|
96
136
|
|
|
97
|
-
|
|
137
|
+
**With a block (`.which`)** when you need full control:
|
|
98
138
|
|
|
99
139
|
```ruby
|
|
100
|
-
expect { User.create!(username: "BOB", password: "
|
|
140
|
+
expect { User.create!(username: "BOB", password: "secret") }
|
|
101
141
|
.to create_a(User)
|
|
102
142
|
.which { |user|
|
|
103
143
|
expect(user.encrypted_password).to be_present
|
|
104
|
-
expect(
|
|
144
|
+
expect(Auth.authenticate("bob", "secret")).to eq user
|
|
105
145
|
}
|
|
106
146
|
```
|
|
107
147
|
|
|
108
|
-
**
|
|
148
|
+
**That the block returns the new record (`.and_return_it`):**
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
expect { create_user(name: "bob") }
|
|
152
|
+
.to create_a(User)
|
|
153
|
+
.with_attributes(name: "bob")
|
|
154
|
+
.and_return_it
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Negated:** use `.not_to create_a(User)` to assert the block did *not* create exactly one `User` (e.g. created zero or more than one).
|
|
158
|
+
|
|
159
|
+
**Failure cases:**
|
|
109
160
|
|
|
110
|
-
|
|
161
|
+
- Creates 0 → *"the block should have created 1 User, but created 0"*
|
|
162
|
+
- Creates 2+ → *"the block should have created 1 User, but created 2"*
|
|
163
|
+
- Attribute mismatch → *"Expected :username to be \"bob\", but was \"BOB\""*
|
|
164
|
+
- `.which` or `.which_is_expected_to` fail → their messages are shown
|
|
111
165
|
|
|
112
|
-
|
|
166
|
+
---
|
|
113
167
|
|
|
114
|
-
|
|
168
|
+
### create / create_records
|
|
169
|
+
|
|
170
|
+
Asserts the block creates **exactly** the given counts per model. Argument is a hash: `Model => count`.
|
|
171
|
+
|
|
172
|
+
**Minimal:**
|
|
115
173
|
|
|
116
174
|
```ruby
|
|
117
175
|
expect { User.create!; User.create!; Profile.create! }
|
|
118
176
|
.to create(User => 2, Profile => 1)
|
|
119
177
|
```
|
|
120
178
|
|
|
121
|
-
|
|
179
|
+
**With attributes:** provide one hash per created record. Keys are model classes; values are arrays of attribute hashes. Order of hashes need not match creation order.
|
|
122
180
|
|
|
123
181
|
```ruby
|
|
124
|
-
expect {
|
|
125
|
-
.to create(User =>
|
|
182
|
+
expect { User.create!(username: "bob"); User.create!(username: "rhonda") }
|
|
183
|
+
.to create(User => 2)
|
|
126
184
|
.with_attributes(
|
|
127
|
-
User => [{username: "bob"}]
|
|
128
|
-
|
|
129
|
-
).which { |records|
|
|
130
|
-
# records is a hash with model classes for keys and the new records for values
|
|
131
|
-
new_user = records[User].first
|
|
132
|
-
new_profile = records[Profile].first
|
|
133
|
-
expect(AuthLibrary.authenticate("bob", "BlueSteel45")).to eq new_user
|
|
134
|
-
expect(new_user.profile).to eq new_profile
|
|
135
|
-
}
|
|
185
|
+
User => [{ username: "rhonda" }, { username: "bob" }]
|
|
186
|
+
)
|
|
136
187
|
```
|
|
137
188
|
|
|
138
|
-
|
|
189
|
+
You must supply **as many attribute hashes as the expected count** for that model. Fewer raises an argument error; you can use empty hashes for records you don’t care to constrain:
|
|
139
190
|
|
|
140
|
-
|
|
191
|
+
```ruby
|
|
192
|
+
.with_attributes(User => [{ username: "bob" }, {}])
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**With a block (`.which`):** the block receives a hash `Model => [records]`:
|
|
141
196
|
|
|
142
197
|
```ruby
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
.to create(User => 2)
|
|
198
|
+
expect { sign_up!("bob", "secret") }
|
|
199
|
+
.to create(User => 1, Profile => 1)
|
|
146
200
|
.with_attributes(
|
|
147
|
-
User
|
|
201
|
+
User => [{ username: "bob" }],
|
|
202
|
+
Profile => [{ avatar_url: Profile.default_avatar_url }]
|
|
148
203
|
)
|
|
204
|
+
.which { |records|
|
|
205
|
+
user = records[User].first
|
|
206
|
+
profile = records[Profile].first
|
|
207
|
+
expect(user.profile).to eq profile
|
|
208
|
+
}
|
|
209
|
+
```
|
|
149
210
|
|
|
150
|
-
|
|
151
|
-
expect { User.create!(username: "bob"); User.create!(username: "rhonda") }
|
|
152
|
-
.to create(User => 2)
|
|
153
|
-
.with_attributes(
|
|
154
|
-
User => [{username: "rhonda"}]
|
|
155
|
-
)
|
|
211
|
+
**That the block returns all created records (`.and_return_them`):** the matcher checks that every created record appears in the block’s return value (array, relation, or any enumerable).
|
|
156
212
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
213
|
+
```ruby
|
|
214
|
+
expect { [User.create!, User.create!] }.to create(User => 2).and_return_them
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Negated:** `.not_to create(User => 2)` asserts the block did not create exactly two `User`s.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
### create_associated
|
|
222
|
+
|
|
223
|
+
Asserts that new record(s) were created **within the given association scope(s)** only. The model is inferred from the scope (e.g. `user.posts` → `Post`). Other associations and pre-existing records are ignored.
|
|
224
|
+
|
|
225
|
+
**Scope form (expects exactly one new record in that association):**
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
expect { user.posts.create!(title: "Hi") }.to create_associated(user.posts)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Hash form (expects the given count per scope):**
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
expect {
|
|
235
|
+
user.posts.create!(title: "A")
|
|
236
|
+
user.posts.create!(title: "B")
|
|
237
|
+
}.to create_associated(user.posts => 2)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**With attributes**
|
|
241
|
+
|
|
242
|
+
- One scope, one record: pass a single hash.
|
|
243
|
+
- One scope, many records: pass an array of hashes.
|
|
244
|
+
- Multiple scopes: pass a hash keyed by scope, each value an array of attribute hashes.
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
# One record, single hash
|
|
248
|
+
expect { user.posts.create!(title: "Hi") }
|
|
249
|
+
.to create_associated(user.posts)
|
|
250
|
+
.with_attributes(title: "Hi")
|
|
251
|
+
|
|
252
|
+
# One scope, multiple records
|
|
253
|
+
expect { add_two_posts }
|
|
254
|
+
.to create_associated(user.posts => 2)
|
|
255
|
+
.with_attributes([{ title: "First" }, { title: "Second" }])
|
|
256
|
+
|
|
257
|
+
# Multiple scopes
|
|
258
|
+
expect { create_mine_and_theirs }
|
|
259
|
+
.to create_associated(user_a.posts => 1, user_b.posts => 1)
|
|
161
260
|
.with_attributes(
|
|
162
|
-
|
|
261
|
+
user_a.posts => [{ title: "a" }],
|
|
262
|
+
user_b.posts => [{ title: "b" }]
|
|
163
263
|
)
|
|
164
264
|
```
|
|
165
265
|
|
|
166
|
-
|
|
266
|
+
**With `.which`:** the block receives a hash **model class => records** (same shape as `create` / `create_records`):
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
expect { user.posts.create!(title: "x") }
|
|
270
|
+
.to create_associated(user.posts)
|
|
271
|
+
.which { |records_by_klass|
|
|
272
|
+
expect(records_by_klass[Post].first.title).to eq "x"
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Return value:**
|
|
277
|
+
|
|
278
|
+
- Use **`.and_return_it`** only when expecting **exactly one** record (single scope, count 1). It asserts the block returns that record.
|
|
279
|
+
- Use **`.and_return_them`** when expecting multiple records; it asserts the block’s return value contains all created records.
|
|
280
|
+
|
|
281
|
+
Using `.and_return_it` with multiple records (e.g. `create_associated(scope => 2).and_return_it`) raises `ArgumentError`.
|
|
167
282
|
|
|
168
|
-
|
|
283
|
+
**Failure cases:**
|
|
169
284
|
|
|
170
|
-
|
|
285
|
+
- No records in scope → *"The block should have created 1 Post within the scope, but created 0."*
|
|
286
|
+
- Records created in a *different* association (or by another owner) are not counted—you’ll get “created 0” if the wrong scope was used.
|
|
287
|
+
- Wrong count or attribute mismatch produces messages similar to `create` / `create_records`.
|
|
171
288
|
|
|
172
|
-
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Record retrieval strategies
|
|
292
|
+
|
|
293
|
+
The matchers need to know which rows were “new” after the block. Two strategies are built in:
|
|
294
|
+
|
|
295
|
+
| Strategy | How it finds new records | Default | When to use it |
|
|
296
|
+
|-------------|--------------------------|----------|-----------------|
|
|
297
|
+
| **`:id`** | Compares max primary key before/after the block | ✓ | Tables with auto-increment integer PKs (default). |
|
|
298
|
+
| **`:timestamp`** | Compares `created_at` (or configured column) before/after; supports frozen time | — | Non-integer PKs or no `id`. |
|
|
299
|
+
|
|
300
|
+
**`:id`** is the default because it doesn’t depend on clock precision or time mocking. Use **`:timestamp`** when you don’t have a monotonic `id` (e.g. UUIDs, legacy schemas).
|
|
301
|
+
|
|
302
|
+
Strategy can be set globally in [configuration](#configuration) or overridden per expectation:
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
expect { Person.create! }.to create_a(Person, strategy: :timestamp)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`create_associated` uses the configured default strategy only (it has no per-call strategy option).
|
|
309
|
+
|
|
310
|
+
---
|
|
173
311
|
|
|
174
312
|
## Configuration
|
|
175
313
|
|
|
176
|
-
|
|
314
|
+
Configure column names and default strategy in your RSpec setup (e.g. `spec/rails_helper.rb` or `spec_helper.rb`):
|
|
177
315
|
|
|
178
316
|
```ruby
|
|
179
317
|
ActiveRecordChangeMatchers::Config.configure do |config|
|
|
180
|
-
|
|
181
|
-
# default value is "id"
|
|
318
|
+
# Primary key column for :id strategy (default: "id")
|
|
182
319
|
config.id_column_name = "primary_key"
|
|
183
320
|
|
|
184
|
-
#
|
|
321
|
+
# Timestamp column for :timestamp strategy (default: "created_at")
|
|
185
322
|
config.created_at_column_name = "created_timestamp"
|
|
186
323
|
|
|
187
|
-
#
|
|
188
|
-
# must be one of [:id, :timestamp]
|
|
324
|
+
# Default strategy: :id or :timestamp (default: :id)
|
|
189
325
|
config.default_strategy = :timestamp
|
|
190
326
|
end
|
|
191
327
|
```
|
|
192
328
|
|
|
193
|
-
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Tips and gotchas
|
|
332
|
+
|
|
333
|
+
**Block syntax with `.which`**
|
|
334
|
+
|
|
335
|
+
Use braces `{ }` for the block passed to `.which`. With `do ... end`, Ruby binds the block to `expect(...).to(...)` instead of `.which`, so the block may never run and the test can falsely pass:
|
|
194
336
|
|
|
195
337
|
```ruby
|
|
196
|
-
|
|
338
|
+
# Prefer:
|
|
339
|
+
.to create_a(User).which { |user| expect(user.name).to eq "bob" }
|
|
340
|
+
|
|
341
|
+
# Parsing trap with do/end:
|
|
342
|
+
.to create_a(User).which do |user|
|
|
343
|
+
expect(user.name).to eq "bob" # this block is not passed to .which
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Composable matchers**
|
|
348
|
+
|
|
349
|
+
`with_attributes` accepts RSpec composable matchers, which keeps specs readable and failure messages clear:
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
.with_attributes(
|
|
353
|
+
username: a_string_matching(/\A[a-z]+\z/),
|
|
354
|
+
age: be_between(18, 120)
|
|
355
|
+
)
|
|
197
356
|
```
|
|
198
357
|
|
|
358
|
+
**Attributes and virtual/derived values**
|
|
359
|
+
|
|
360
|
+
`with_attributes` uses `record.public_send(field)` for each key, so you can assert on any public reader—database columns, delegations, or methods (e.g. `full_name` built from `first_name` and `last_name`).
|
|
361
|
+
|
|
362
|
+
---
|
|
199
363
|
|
|
200
364
|
## Development
|
|
201
365
|
|
|
202
|
-
|
|
366
|
+
```bash
|
|
367
|
+
git clone https://github.com/Darhazer/active_record_change_matchers
|
|
368
|
+
cd active_record_change_matchers
|
|
369
|
+
bin/setup
|
|
370
|
+
bundle exec rspec
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
To install the gem locally:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
bundle exec rake install
|
|
377
|
+
```
|
|
203
378
|
|
|
204
|
-
|
|
379
|
+
---
|
|
205
380
|
|
|
206
381
|
## Contributing
|
|
207
382
|
|
|
208
|
-
1. Fork
|
|
209
|
-
2. Create
|
|
210
|
-
3. Commit
|
|
211
|
-
4. Push
|
|
212
|
-
5.
|
|
383
|
+
1. Fork the repo.
|
|
384
|
+
2. Create a feature branch: `git checkout -b my-feature`
|
|
385
|
+
3. Commit changes: `git commit -am 'Add my feature'`
|
|
386
|
+
4. Push: `git push origin my-feature`
|
|
387
|
+
5. Open a Pull Request against this repository.
|
|
213
388
|
|
|
389
|
+
---
|
|
214
390
|
|
|
215
|
-
|
|
391
|
+
**License:** MIT.
|
|
392
|
+
**Changelog:** [CHANGELOG.md](CHANGELOG.md).
|
data/Rakefile
CHANGED
|
@@ -1,33 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
lib = File.expand_path("../lib", __FILE__)
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
-
require
|
|
3
|
+
require 'active_record_change_matchers/version'
|
|
5
4
|
|
|
6
5
|
Gem::Specification.new do |spec|
|
|
7
|
-
spec.name =
|
|
6
|
+
spec.name = 'active_record_change_matchers'
|
|
8
7
|
spec.version = ActiveRecordChangeMatchers::VERSION
|
|
9
|
-
spec.authors = [
|
|
10
|
-
spec.email = [
|
|
8
|
+
spec.authors = ['Maxim Krizhanovski', 'Nathan Wallace']
|
|
9
|
+
spec.email = ['maxim.krizhanovski@hey.com']
|
|
11
10
|
|
|
12
|
-
spec.summary =
|
|
13
|
-
spec.description =
|
|
14
|
-
spec.homepage =
|
|
15
|
-
spec.license =
|
|
11
|
+
spec.summary = 'Additional RSpec custom matchers for ActiveRecord'
|
|
12
|
+
spec.description = 'This gem adds custom block expectation matchers for RSpec, such as `expect { ... }.to create_a_new(User)`'
|
|
13
|
+
spec.homepage = 'https://github.com/Darhazer/active_record_change_matchers'
|
|
14
|
+
spec.license = 'MIT'
|
|
16
15
|
|
|
17
16
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
18
|
-
spec.bindir =
|
|
17
|
+
spec.bindir = 'exe'
|
|
19
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
20
|
-
spec.require_paths = [
|
|
21
|
-
spec.required_ruby_version =
|
|
19
|
+
spec.require_paths = ['lib']
|
|
20
|
+
spec.required_ruby_version = '>= 3.3.0'
|
|
22
21
|
|
|
23
|
-
spec.add_dependency
|
|
24
|
-
spec.add_dependency
|
|
22
|
+
spec.add_dependency 'activerecord', '>= 7.0'
|
|
23
|
+
spec.add_dependency 'rspec-expectations', '>= 3.0.0'
|
|
25
24
|
|
|
26
|
-
spec.add_development_dependency
|
|
27
|
-
spec.add_development_dependency
|
|
28
|
-
spec.add_development_dependency
|
|
29
|
-
spec.add_development_dependency
|
|
30
|
-
spec.add_development_dependency
|
|
31
|
-
spec.add_development_dependency
|
|
32
|
-
spec.add_development_dependency
|
|
25
|
+
spec.add_development_dependency 'database_cleaner'
|
|
26
|
+
spec.add_development_dependency 'pry'
|
|
27
|
+
spec.add_development_dependency 'rake'
|
|
28
|
+
spec.add_development_dependency 'rspec'
|
|
29
|
+
spec.add_development_dependency 'sqlite3'
|
|
30
|
+
spec.add_development_dependency 'standalone_migrations'
|
|
31
|
+
spec.add_development_dependency 'timecop'
|
|
33
32
|
end
|