active_record_change_matchers 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +14 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +215 -0
- data/Rakefile +16 -0
- data/active_record_block_matchers.gemspec +33 -0
- data/active_record_change_matchers.gemspec +33 -0
- data/bin/console +11 -0
- data/bin/setup +7 -0
- data/db/config.yml +11 -0
- data/db/migrate/20150225014908_create_people.rb +9 -0
- data/db/migrate/20151017231107_create_dogs.rb +9 -0
- data/db/schema.rb +29 -0
- data/lib/active_record_block_matchers.rb +7 -0
- data/lib/active_record_change_matchers/config.rb +32 -0
- data/lib/active_record_change_matchers/create_a_new_matcher.rb +94 -0
- data/lib/active_record_change_matchers/create_records_matcher.rb +112 -0
- data/lib/active_record_change_matchers/strategies/id_strategy.rb +29 -0
- data/lib/active_record_change_matchers/strategies/timestamp_strategy.rb +32 -0
- data/lib/active_record_change_matchers/strategies.rb +31 -0
- data/lib/active_record_change_matchers/version.rb +3 -0
- data/lib/active_record_change_matchers.rb +7 -0
- metadata +196 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3e26db33cc55cb3efca1b3ef9824ce2e2c363636ef781f355e907afba1b68402
|
4
|
+
data.tar.gz: d322588b1fd5464acf44cdd75d5593fa64325cc5690d0d69f7eb002feacfff9c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 84e0d2efa264f21de6648b8fe8ed80591eec15376b56904115c48b0ea1123d759a1908c328736d1a4271a4b9e11cae255c67af61871167c987eb3ac4df2f5060
|
7
|
+
data.tar.gz: 2095854b7ff7cfc1f26ef4e052b282af373432d5d079c192bf57d2e23b8e0a401421f958776cc82413f9c2ecca633a029b70593bd1b03e9046a662e5cfbb1550
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## Master (Unreleased)
|
4
|
+
|
5
|
+
## 1.0.0 (2024-07-03)
|
6
|
+
|
7
|
+
Hard fork. Changes since the original version:
|
8
|
+
- Support for frozen time when working with the Timestamp strategy ([original issue](https://github.com/nwallace/active_record_block_matchers/issues/10))
|
9
|
+
- which_is_expected_to modifier modifier ([original issue](https://github.com/nwallace/active_record_block_matchers/issues/9))
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Nathan Wallace
|
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,215 @@
|
|
1
|
+
# active_record_change_matchers
|
2
|
+
|
3
|
+
Custom RSpec matchers for ActiveRecord record creation.
|
4
|
+
This is a hard-fork of [active_record_block_matchers](https://github.com/nwallace/active_record_block_matchers). See [the changelog](CHANGELOG.md) for changes since the original gem was written.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'active_record_change_matchers'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install active_record_change_matchers
|
21
|
+
|
22
|
+
## Quick Examples
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
expect {
|
26
|
+
post :create, user: { username: "bob", password: "BlueSteel45" }
|
27
|
+
}.to create_a(User)
|
28
|
+
.with_attributes(username: "bob")
|
29
|
+
.which {|bob| expect(AuthLibrary.authenticate("bob", "BlueSteel45")).to eq bob }
|
30
|
+
|
31
|
+
expect {
|
32
|
+
post :create, user: { username: "bob", password: "BlueSteel45" }
|
33
|
+
}.to create(User => 1, Profile => 1)
|
34
|
+
.with_attributes(
|
35
|
+
User => [{username: "bob"}],
|
36
|
+
Profile => [{avatar_url: Avatar.default_avatar_url}],
|
37
|
+
).which { |new_records_hash|
|
38
|
+
new_user = new_records_hash[User].first
|
39
|
+
new_profile = new_records_hash[Profile].first
|
40
|
+
expect(new_user.profile).to eq new_profile
|
41
|
+
}
|
42
|
+
```
|
43
|
+
|
44
|
+
## Detailed Examples
|
45
|
+
|
46
|
+
#### `create_a`
|
47
|
+
|
48
|
+
aliases: `create_an`, `create_a_new`
|
49
|
+
|
50
|
+
Example:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
expect { User.create! }.to create_a(User)
|
54
|
+
```
|
55
|
+
|
56
|
+
This can be very useful for controller tests:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
expect { post :create, user: user_params }.to create_a(User)
|
60
|
+
```
|
61
|
+
|
62
|
+
You can chain `.with_attributes` as well to define a list of values you expect the new object to have. This works with both database attributes and computed values.
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
expect { User.create!(username: "bob") }
|
66
|
+
.to create_a(User)
|
67
|
+
.with_attributes(username: "bob")
|
68
|
+
```
|
69
|
+
|
70
|
+
This is a great way to test ActiveReocrd hooks on your model. For example, if your User model downcases all usernames before saving them to the database, you can test it like this:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
expect { User.create!(username: "BOB") }
|
74
|
+
.to create_a(User)
|
75
|
+
.with_attributes(username: "bob")
|
76
|
+
```
|
77
|
+
|
78
|
+
You can even use RSpec's [composable matchers][^1]:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
expect { User.create!(username: "bob") }
|
82
|
+
.to create_a(User)
|
83
|
+
.with_attributes(username: a_string_starting_with("b"))
|
84
|
+
```
|
85
|
+
|
86
|
+
If you need to make assertions about things other than attribute equality, you can also chain `.which_is_expected_to` with a (composable) matcher:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
expect { User.create!(username: "BOB", password: "BlueSteel45") }
|
90
|
+
.to create_a(User)
|
91
|
+
.which_is_expected_to(
|
92
|
+
have_attributes(encrypted_password: be_present)
|
93
|
+
.and(eq(AuthLibrary.authenticate("bob", "BlueSteel45")))
|
94
|
+
)
|
95
|
+
```
|
96
|
+
|
97
|
+
If that's doesn't provide enough flexibility, you can also chain `.which` with a block, and your block will receive the newly created record:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
expect { User.create!(username: "BOB", password: "BlueSteel45") }
|
101
|
+
.to create_a(User)
|
102
|
+
.which { |user|
|
103
|
+
expect(user.encrypted_password).to be_present
|
104
|
+
expect(AuthLibrary.authenticate("bob", "BlueSteel45")).to eq user
|
105
|
+
}
|
106
|
+
```
|
107
|
+
|
108
|
+
**Gotcha Warning:** Be careful about your block syntax when chaining `.which` in your tests. If you write the above example with a `do...end`, the example will parse like this: `expect {...}.to(create_a(User).which) do |user| ... end`, so your block will not execute, and it may appear that your test is passing, when it is not.
|
109
|
+
|
110
|
+
#### `create`
|
111
|
+
|
112
|
+
aliases: `create_records`
|
113
|
+
|
114
|
+
Example:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
expect { User.create!; User.create!; Profile.create! }
|
118
|
+
.to create(User => 2, Profile => 1)
|
119
|
+
```
|
120
|
+
|
121
|
+
Just like the other matcher, you can chain `with_attributes` and `which` to assert about the particulars of the records:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
expect { UserService.sign_up!(username: "bob", password: "BlueSteel45") }
|
125
|
+
.to create(User => 1, Profile => 1)
|
126
|
+
.with_attributes(
|
127
|
+
User => [{username: "bob"}],
|
128
|
+
Profile => [{avatar_url: Avatar.default_avatar_url}]
|
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
|
+
}
|
136
|
+
```
|
137
|
+
|
138
|
+
As noted, the `which` block yields a hash containing the new records whose counts were specified.
|
139
|
+
|
140
|
+
Order doesn't matter for the attributes specified in `with_attributes`, but you must provide an attribute hash for every record that was created. This means, if you expect the block to create, say 2 User records, you must provide an attributes hash for each new User record:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
# This is correct:
|
144
|
+
expect { User.create!(username: "bob"); User.create!(username: "rhonda") }
|
145
|
+
.to create(User => 2)
|
146
|
+
.with_attributes(
|
147
|
+
User => [{username: "rhonda"}, {username: "bob"}]
|
148
|
+
)
|
149
|
+
|
150
|
+
# This will raise an error:
|
151
|
+
expect { User.create!(username: "bob"); User.create!(username: "rhonda") }
|
152
|
+
.to create(User => 2)
|
153
|
+
.with_attributes(
|
154
|
+
User => [{username: "rhonda"}]
|
155
|
+
)
|
156
|
+
|
157
|
+
# But this is totally fine if you really need a workaround:
|
158
|
+
# Just put the empty hashes last
|
159
|
+
expect { User.create!(username: "bob"); User.create!(username: "rhonda") }
|
160
|
+
.to create(User => 2)
|
161
|
+
.with_attributes(
|
162
|
+
User => [{username: "rhonda"}, {}]
|
163
|
+
)
|
164
|
+
```
|
165
|
+
|
166
|
+
## Record Retrieval Strategies
|
167
|
+
|
168
|
+
There are currently two retrieval strategies implemented: `:id` and `:timestamp`. `:id` is the default, but this can be configured via the `default_strategy` configuration variable (more details [below](#configuration)).
|
169
|
+
|
170
|
+
The ID and Timestamp Strategies work similarly. The ID Strategy queries the appropriate table(s) to find the highest ID value(s) before the block, then finds new records by looking for records with an ID that higher than that. The Timestamp Strategy uses `Time.current` to record the time before the block. Then it finds new records by looking for records that have a timestamp later than that.
|
171
|
+
|
172
|
+
The ID Strategy is the default because it doesn't rely on time values that may be imprecise or mocked out. The Timestamp Strategy is useful if your tables don't have autoincrementing integer primary keys.
|
173
|
+
|
174
|
+
## Configuration
|
175
|
+
|
176
|
+
You can configure the column names used by the ID or Timestamp Strategies. Put code like this in your `spec_helper.rb` or similar file:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
ActiveRecordChangeMatchers::Config.configure do |config|
|
180
|
+
|
181
|
+
# default value is "id"
|
182
|
+
config.id_column_name = "primary_key"
|
183
|
+
|
184
|
+
# default value is "created_at"
|
185
|
+
config.created_at_column_name = "created_timestamp"
|
186
|
+
|
187
|
+
# default value is :id
|
188
|
+
# must be one of [:id, :timestamp]
|
189
|
+
config.default_strategy = :timestamp
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
You can also override the default strategy for individual assertions if needed:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
expect { Person.create! }.to create_a(Person, strategy: :id)
|
197
|
+
```
|
198
|
+
|
199
|
+
|
200
|
+
## Development
|
201
|
+
|
202
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
203
|
+
|
204
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
205
|
+
|
206
|
+
## Contributing
|
207
|
+
|
208
|
+
1. Fork it ( https://github.com/[my-github-username]/active_record_change_matchers/fork )
|
209
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
210
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
211
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
212
|
+
5. Create a new Pull Request
|
213
|
+
|
214
|
+
|
215
|
+
[^1]: https://rspec.info/features/3-13/rspec-expectations/composing-matchers/
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "standalone_migrations"
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
StandaloneMigrations::Tasks.load_tasks
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
8
|
+
t.pattern = Dir.glob('spec/**/*_spec.rb')
|
9
|
+
t.rspec_opts = '--format documentation'
|
10
|
+
end
|
11
|
+
|
12
|
+
task :travis do
|
13
|
+
Rake::Task['db:setup'].invoke
|
14
|
+
Rake::Task[:spec].invoke
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "active_record_change_matchers/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "active_record_change_matchers"
|
8
|
+
spec.version = ActiveRecordChangeMatchers::VERSION
|
9
|
+
spec.authors = ["Maxim Krizhanovski", "Nathan Wallace"]
|
10
|
+
spec.email = ["maxim.krizhanovski@hey.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Additional RSpec custom matchers for ActiveRecord}
|
13
|
+
spec.description = %q{This gem adds custom block expectation matchers for RSpec, such as `expect { ... }.to create_a_new(User)`}
|
14
|
+
spec.homepage = "https://github.com/Darhazer/active_record_change_matchers"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
spec.required_ruby_version = ">= 1.9.3"
|
22
|
+
|
23
|
+
spec.add_dependency "activerecord", ">= 3.2.0"
|
24
|
+
spec.add_dependency "rspec-expectations", ">= 3.0.0"
|
25
|
+
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.6"
|
28
|
+
spec.add_development_dependency "pry", "~> 0.10"
|
29
|
+
spec.add_development_dependency "sqlite3", "~> 1.3"
|
30
|
+
spec.add_development_dependency "database_cleaner", "~> 1.6"
|
31
|
+
spec.add_development_dependency "standalone_migrations", "~> 5.2"
|
32
|
+
spec.add_development_dependency "timecop", "~> 0.9"
|
33
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "active_record_change_matchers/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "active_record_change_matchers"
|
8
|
+
spec.version = ActiveRecordChangeMatchers::VERSION
|
9
|
+
spec.authors = ["Maxim Krizhanovski", "Nathan Wallace"]
|
10
|
+
spec.email = ["maxim.krizhanovski@hey.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Additional RSpec custom matchers for ActiveRecord}
|
13
|
+
spec.description = %q{This gem adds custom block expectation matchers for RSpec, such as `expect { ... }.to create_a_new(User)`}
|
14
|
+
spec.homepage = "https://github.com/Darhazer/active_record_change_matchers"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
spec.required_ruby_version = ">= 1.9.3"
|
22
|
+
|
23
|
+
spec.add_dependency "activerecord", ">= 3.2.0"
|
24
|
+
spec.add_dependency "rspec-expectations", ">= 3.0.0"
|
25
|
+
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.6"
|
28
|
+
spec.add_development_dependency "pry", "~> 0.10"
|
29
|
+
spec.add_development_dependency "sqlite3", "~> 1.3"
|
30
|
+
spec.add_development_dependency "database_cleaner", "~> 1.6"
|
31
|
+
spec.add_development_dependency "standalone_migrations", "~> 5.2"
|
32
|
+
spec.add_development_dependency "timecop", "~> 0.9"
|
33
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "active_record_change_matchers"
|
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
|
data/bin/setup
ADDED
data/db/config.yml
ADDED
data/db/schema.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# This file is auto-generated from the current state of the database. Instead
|
2
|
+
# of editing this file, please use the migrations feature of Active Record to
|
3
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
4
|
+
#
|
5
|
+
# Note that this schema.rb definition is the authoritative source for your
|
6
|
+
# database schema. If you need to create the application database on another
|
7
|
+
# system, you should be using db:schema:load, not running all the migrations
|
8
|
+
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
9
|
+
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
10
|
+
#
|
11
|
+
# It's strongly recommended that you check this file into your version control system.
|
12
|
+
|
13
|
+
ActiveRecord::Schema.define(version: 20151017231107) do
|
14
|
+
|
15
|
+
create_table "dogs", force: :cascade do |t|
|
16
|
+
t.string "name"
|
17
|
+
t.string "breed"
|
18
|
+
t.datetime "created_at"
|
19
|
+
t.datetime "updated_at"
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table "people", force: :cascade do |t|
|
23
|
+
t.string "first_name"
|
24
|
+
t.string "last_name"
|
25
|
+
t.datetime "created_at"
|
26
|
+
t.datetime "updated_at"
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ActiveRecordChangeMatchers
|
2
|
+
class Config
|
3
|
+
|
4
|
+
def self.configure
|
5
|
+
yield self
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.default_strategy
|
9
|
+
@default_strategy || :id
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.default_strategy=(strategy_key)
|
13
|
+
@default_strategy = strategy_key.to_sym
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.created_at_column_name
|
17
|
+
@created_at_column_name || "created_at"
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.created_at_column_name=(column_name)
|
21
|
+
@created_at_column_name = column_name
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.id_column_name
|
25
|
+
@id_column_name || "id"
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.id_column_name=(column_name)
|
29
|
+
@id_column_name = column_name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
RSpec::Matchers.define :create_a_new do |klass|
|
2
|
+
supports_block_expectations
|
3
|
+
|
4
|
+
description do
|
5
|
+
"create a #{klass}, optionally verifying attributes"
|
6
|
+
end
|
7
|
+
|
8
|
+
chain(:with_attributes) do |attributes|
|
9
|
+
@attributes = attributes
|
10
|
+
end
|
11
|
+
|
12
|
+
chain(:which) do |&block|
|
13
|
+
@which_block = block
|
14
|
+
end
|
15
|
+
|
16
|
+
chain(:which_is_expected_to) do |matcher|
|
17
|
+
@which_matcher = matcher
|
18
|
+
end
|
19
|
+
|
20
|
+
match do |options={}, block|
|
21
|
+
fetching_strategy =
|
22
|
+
ActiveRecordChangeMatchers::Strategies.for_key(options[:strategy]).new(block)
|
23
|
+
|
24
|
+
@created_records = fetching_strategy.new_records([klass])[klass]
|
25
|
+
|
26
|
+
return false unless @created_records.count == 1 # ? this shouldn't be necessary for all strategies...
|
27
|
+
|
28
|
+
record = @created_records.first
|
29
|
+
|
30
|
+
@attribute_mismatches = []
|
31
|
+
|
32
|
+
@attributes && @attributes.each do |field, value|
|
33
|
+
unless values_match?(value, record.public_send(field))
|
34
|
+
@attribute_mismatches << [field, value, record.public_send(field)]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
if @attribute_mismatches.none? && @which_block
|
39
|
+
begin
|
40
|
+
@which_block.call(record)
|
41
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
42
|
+
@which_failure = e
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
if @attribute_mismatches.none? && @which_matcher
|
47
|
+
unless @which_matcher.matches?(record)
|
48
|
+
@matcher_failure = @which_matcher.failure_message
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
@attribute_mismatches.empty? && @which_failure.nil? && @matcher_failure.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
failure_message do
|
56
|
+
if @created_records.count != 1
|
57
|
+
"the block should have created 1 #{klass}, but created #{@created_records.count}"
|
58
|
+
elsif @attribute_mismatches.any?
|
59
|
+
@attribute_mismatches.map do |field, expected, actual|
|
60
|
+
expected_description = is_composable_matcher?(expected) ? expected.description : expected.inspect
|
61
|
+
"Expected #{field.inspect} to be #{expected_description}, but was #{actual.inspect}"
|
62
|
+
end.join("\n")
|
63
|
+
elsif @which_failure
|
64
|
+
@which_failure.message
|
65
|
+
else
|
66
|
+
@matcher_failure
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
failure_message_when_negated do
|
71
|
+
if @created_records.count == 1 && @attributes && @attribute_mismatches.none?
|
72
|
+
"the block should not have created a #{klass} with attributes #{format_attributes_hash(@attributes).inspect}, but did"
|
73
|
+
elsif @created_records.count == 1 && @which_block && !@which_failure
|
74
|
+
"the newly created #{klass} should have failed an expectation in the given block, but didn't"
|
75
|
+
elsif @created_records.count == 1
|
76
|
+
"the block should not have created a #{klass}, but created #{@created_records.count}: #{@created_records.inspect}"
|
77
|
+
else
|
78
|
+
"the block created a #{klass} that matched all given criteria"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def is_composable_matcher?(value)
|
83
|
+
value.respond_to?(:failure_message_when_negated)
|
84
|
+
end
|
85
|
+
|
86
|
+
def format_attributes_hash(attributes)
|
87
|
+
attributes.each_with_object({}) do |(field,value), memo|
|
88
|
+
memo[field] = is_composable_matcher?(value) ? value.description : value
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
RSpec::Matchers.alias_matcher :create_a, :create_a_new
|
94
|
+
RSpec::Matchers.alias_matcher :create_an, :create_a_new
|
@@ -0,0 +1,112 @@
|
|
1
|
+
RSpec::Matchers.define :create_records do |record_counts|
|
2
|
+
include ActiveSupport::Inflector
|
3
|
+
|
4
|
+
supports_block_expectations
|
5
|
+
|
6
|
+
description do
|
7
|
+
counts_strs = record_counts.map { |klass, count| count_str(klass, count) }
|
8
|
+
"create #{counts_strs.join(", ")}"
|
9
|
+
end
|
10
|
+
|
11
|
+
chain(:with_attributes) do |attributes|
|
12
|
+
if mismatch=attributes.find {|klass, hashes| hashes.size != record_counts[klass]}
|
13
|
+
mismatched_class, hashes = mismatch
|
14
|
+
raise ArgumentError, "Specified the block should create #{record_counts[mismatched_class]} #{mismatched_class}, but provided #{hashes.size} #{mismatched_class} attribute specifications"
|
15
|
+
end
|
16
|
+
@expected_attributes = attributes
|
17
|
+
end
|
18
|
+
|
19
|
+
chain(:which) do |&block|
|
20
|
+
@which_block = block
|
21
|
+
end
|
22
|
+
|
23
|
+
match do |options={}, block|
|
24
|
+
fetching_strategy =
|
25
|
+
ActiveRecordChangeMatchers::Strategies.for_key(options[:strategy]).new(block)
|
26
|
+
|
27
|
+
@new_records = fetching_strategy.new_records(record_counts.keys)
|
28
|
+
|
29
|
+
@incorrect_counts =
|
30
|
+
@new_records.each_with_object({}) do |(klass, new_records), incorrect|
|
31
|
+
actual_count = new_records.count
|
32
|
+
expected_count = record_counts[klass]
|
33
|
+
if actual_count != expected_count
|
34
|
+
incorrect[klass] = { expected: expected_count, actual: actual_count }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
return false if @incorrect_counts.any?
|
39
|
+
|
40
|
+
if @expected_attributes
|
41
|
+
@matched_records = Hash.new {|hash, key| hash[key] = []}
|
42
|
+
@all_attributes = Hash.new {|hash, key| hash[key] = []}
|
43
|
+
@incorrect_attributes =
|
44
|
+
@expected_attributes.each_with_object(Hash.new {|hash, key| hash[key] = []}) do |(klass, expected_attributes), incorrect|
|
45
|
+
@all_attributes[klass] = expected_attributes.map(&:keys).flatten.uniq
|
46
|
+
expected_attributes.each do |expected_attrs|
|
47
|
+
matched_record = (@new_records.fetch(klass) - @matched_records[klass]).find do |record|
|
48
|
+
expected_attrs.all? {|k,v| values_match?(v, record.public_send(k))}
|
49
|
+
end
|
50
|
+
if matched_record
|
51
|
+
@matched_records[klass] << matched_record
|
52
|
+
else
|
53
|
+
incorrect[klass] << expected_attrs
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
@unmatched_records = @matched_records.map {|klass, records| [klass, @new_records[klass] - records]}.to_h.reject {|k,v| v.empty?}
|
58
|
+
return false if @incorrect_attributes.any?
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
begin
|
63
|
+
@which_block && @which_block.call(@new_records)
|
64
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
65
|
+
@which_failure = e
|
66
|
+
end
|
67
|
+
|
68
|
+
@which_failure.nil?
|
69
|
+
end
|
70
|
+
|
71
|
+
failure_message do
|
72
|
+
if @incorrect_counts.present?
|
73
|
+
@incorrect_counts.map do |klass, counts|
|
74
|
+
"The block should have created #{count_str(klass, counts[:expected])}, but created #{counts[:actual]}."
|
75
|
+
end.join(" ")
|
76
|
+
elsif @incorrect_attributes.present?
|
77
|
+
"The block should have created:\n" +
|
78
|
+
@expected_attributes.map do |klass, attrs|
|
79
|
+
" #{attrs.count} #{klass} with these attributes:\n" +
|
80
|
+
attrs.map{|a| " #{a.inspect}"}.join("\n")
|
81
|
+
end.join("\n") +
|
82
|
+
"\nDiff:" +
|
83
|
+
@incorrect_attributes.map do |klass, attrs|
|
84
|
+
"\n Missing #{attrs.count} #{klass} with these attributes:\n" +
|
85
|
+
attrs.map{|a| " #{a.inspect}"}.join("\n")
|
86
|
+
end.join("\n") +
|
87
|
+
@unmatched_records.map do |klass, records|
|
88
|
+
"\n Extra #{records.count} #{klass} with these attributes:\n" +
|
89
|
+
records.map do |r|
|
90
|
+
attrs = @all_attributes[klass].each_with_object({}) {|attr, attrs| attrs[attr] = r.public_send(attr)}
|
91
|
+
" #{attrs.inspect}"
|
92
|
+
end.join("\n")
|
93
|
+
end.join("\n")
|
94
|
+
elsif @which_failure
|
95
|
+
@which_failure
|
96
|
+
else
|
97
|
+
"Unknown error"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
failure_message_when_negated do
|
102
|
+
record_counts.map do |klass, expected_count|
|
103
|
+
"The block should not have created #{count_str(klass, expected_count)}, but created #{expected_count}."
|
104
|
+
end.join(" ")
|
105
|
+
end
|
106
|
+
|
107
|
+
def count_str(klass, count)
|
108
|
+
"#{count} #{klass.name.pluralize(count)}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
RSpec::Matchers.alias_matcher :create, :create_records
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ActiveRecordChangeMatchers
|
2
|
+
class IdStrategy
|
3
|
+
|
4
|
+
def initialize(block)
|
5
|
+
@block = block
|
6
|
+
end
|
7
|
+
|
8
|
+
def new_records(classes)
|
9
|
+
ids_before = classes.each_with_object({}) do |klass, ids_before|
|
10
|
+
ids_before[klass] = klass.select("MAX(#{column_name}) as max_id").take.try(:max_id) || 0
|
11
|
+
end
|
12
|
+
|
13
|
+
block.call
|
14
|
+
|
15
|
+
classes.each_with_object({}) do |klass, new_records|
|
16
|
+
id_before = ids_before[klass]
|
17
|
+
new_records[klass] = klass.where("#{column_name} > ?", id_before).to_a
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :block
|
24
|
+
|
25
|
+
def column_name
|
26
|
+
@column_name ||= ActiveRecordChangeMatchers::Config.id_column_name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ActiveRecordChangeMatchers
|
2
|
+
class TimestampStrategy
|
3
|
+
|
4
|
+
def initialize(block)
|
5
|
+
@block = block
|
6
|
+
end
|
7
|
+
|
8
|
+
def new_records(classes)
|
9
|
+
time_before = Time.current
|
10
|
+
|
11
|
+
existing_records = classes.each_with_object({}) do |klass, hash|
|
12
|
+
hash[klass] = klass.where("#{column_name} = ?", time_before).to_a
|
13
|
+
end
|
14
|
+
|
15
|
+
block.call
|
16
|
+
|
17
|
+
classes.each_with_object({}) do |klass, new_records|
|
18
|
+
new_records[klass] = klass.where("#{column_name} > ?", time_before).to_a +
|
19
|
+
new_records[klass] += klass.where("#{column_name} = ?", time_before).where.not(klass.primary_key => existing_records[klass]).to_a
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :block
|
26
|
+
|
27
|
+
def column_name
|
28
|
+
@column_name ||= ActiveRecordChangeMatchers::Config.created_at_column_name
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module ActiveRecordChangeMatchers
|
2
|
+
class Strategies
|
3
|
+
|
4
|
+
def self.all_strategies
|
5
|
+
@all_strategies ||= {
|
6
|
+
id: IdStrategy,
|
7
|
+
timestamp: TimestampStrategy,
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.default
|
12
|
+
get_strategy!(Config.default_strategy)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.for_key(strategy_key)
|
16
|
+
if strategy_key.nil?
|
17
|
+
default
|
18
|
+
else
|
19
|
+
get_strategy!(strategy_key)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def self.get_strategy!(strategy_key)
|
26
|
+
all_strategies.fetch(strategy_key)
|
27
|
+
rescue KeyError
|
28
|
+
raise UnknownStrategyError, "#{strategy_key.inspect} is not a known strategy (known strategies are #{all_strategies.keys})"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_record_change_matchers
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Maxim Krizhanovski
|
8
|
+
- Nathan Wallace
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2024-07-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 3.2.0
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 3.2.0
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rspec-expectations
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 3.0.0
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 3.0.0
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rake
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '10.0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '10.0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rspec
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '3.6'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '3.6'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: pry
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0.10'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0.10'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: sqlite3
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '1.3'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '1.3'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: database_cleaner
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '1.6'
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '1.6'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: standalone_migrations
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - "~>"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '5.2'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '5.2'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: timecop
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - "~>"
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0.9'
|
133
|
+
type: :development
|
134
|
+
prerelease: false
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - "~>"
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0.9'
|
140
|
+
description: This gem adds custom block expectation matchers for RSpec, such as `expect
|
141
|
+
{ ... }.to create_a_new(User)`
|
142
|
+
email:
|
143
|
+
- maxim.krizhanovski@hey.com
|
144
|
+
executables: []
|
145
|
+
extensions: []
|
146
|
+
extra_rdoc_files: []
|
147
|
+
files:
|
148
|
+
- ".gitignore"
|
149
|
+
- ".rspec"
|
150
|
+
- ".travis.yml"
|
151
|
+
- CHANGELOG.md
|
152
|
+
- Gemfile
|
153
|
+
- LICENSE.txt
|
154
|
+
- README.md
|
155
|
+
- Rakefile
|
156
|
+
- active_record_block_matchers.gemspec
|
157
|
+
- active_record_change_matchers.gemspec
|
158
|
+
- bin/console
|
159
|
+
- bin/setup
|
160
|
+
- db/config.yml
|
161
|
+
- db/migrate/20150225014908_create_people.rb
|
162
|
+
- db/migrate/20151017231107_create_dogs.rb
|
163
|
+
- db/schema.rb
|
164
|
+
- lib/active_record_block_matchers.rb
|
165
|
+
- lib/active_record_change_matchers.rb
|
166
|
+
- lib/active_record_change_matchers/config.rb
|
167
|
+
- lib/active_record_change_matchers/create_a_new_matcher.rb
|
168
|
+
- lib/active_record_change_matchers/create_records_matcher.rb
|
169
|
+
- lib/active_record_change_matchers/strategies.rb
|
170
|
+
- lib/active_record_change_matchers/strategies/id_strategy.rb
|
171
|
+
- lib/active_record_change_matchers/strategies/timestamp_strategy.rb
|
172
|
+
- lib/active_record_change_matchers/version.rb
|
173
|
+
homepage: https://github.com/Darhazer/active_record_change_matchers
|
174
|
+
licenses:
|
175
|
+
- MIT
|
176
|
+
metadata: {}
|
177
|
+
post_install_message:
|
178
|
+
rdoc_options: []
|
179
|
+
require_paths:
|
180
|
+
- lib
|
181
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
182
|
+
requirements:
|
183
|
+
- - ">="
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: 1.9.3
|
186
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
187
|
+
requirements:
|
188
|
+
- - ">="
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: '0'
|
191
|
+
requirements: []
|
192
|
+
rubygems_version: 3.0.3.1
|
193
|
+
signing_key:
|
194
|
+
specification_version: 4
|
195
|
+
summary: Additional RSpec custom matchers for ActiveRecord
|
196
|
+
test_files: []
|