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.
data/db/schema.rb CHANGED
@@ -2,28 +2,36 @@
2
2
  # of editing this file, please use the migrations feature of Active Record to
3
3
  # incrementally modify your database, and then regenerate this schema definition.
4
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).
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema.define(version: 20151017231107) do
14
-
13
+ ActiveRecord::Schema[7.2].define(version: 2016_01_01_000000) do
15
14
  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"
15
+ t.string "name"
16
+ t.string "breed"
17
+ t.datetime "created_at", null: false
18
+ t.datetime "updated_at", null: false
20
19
  end
21
20
 
22
21
  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"
22
+ t.string "first_name"
23
+ t.string "last_name"
24
+ t.datetime "created_at", null: false
25
+ t.datetime "updated_at", null: false
26
+ end
27
+
28
+ create_table "pets", force: :cascade do |t|
29
+ t.integer "person_id", null: false
30
+ t.string "name"
31
+ t.datetime "created_at", null: false
32
+ t.datetime "updated_at", null: false
33
+ t.index ["person_id"], name: "index_pets_on_person_id"
27
34
  end
28
35
 
36
+ add_foreign_key "pets", "people"
29
37
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby 3.4 changed Hash#inspect (symbol keys use {key: val}, other keys use "key" => val with spaces).
4
+ # This helper produces a stable format so error messages look the same on 3.3 and 3.4.
5
+ module ActiveRecordChangeMatchers
6
+ def self.format_hash_for_message(hash)
7
+ "{#{hash.map { |k, v| "#{k.inspect}=>#{v.inspect}" }.join(', ')}}"
8
+ end
9
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordChangeMatchers
4
+ class CreateANewMatcher
5
+ include RSpec::Matchers::Composable
6
+
7
+ def supports_block_expectations?
8
+ true
9
+ end
10
+
11
+ def initialize(klass, options = {})
12
+ @klass = klass
13
+ @strategy_key = options[:strategy]
14
+ end
15
+
16
+ def with_attributes(attributes)
17
+ @attributes = attributes
18
+ self
19
+ end
20
+
21
+ def which(&block)
22
+ @which_block = block
23
+ self
24
+ end
25
+
26
+ def which_is_expected_to(matcher)
27
+ @which_matcher = matcher
28
+ self
29
+ end
30
+
31
+ def and_return_it
32
+ @should_return_record = true
33
+ self
34
+ end
35
+
36
+ def matches?(block)
37
+ @block_return_value = nil
38
+ wrapped_block = proc { @block_return_value = block.call }
39
+ strategy = ActiveRecordChangeMatchers::Strategies.for_key(@strategy_key).new(wrapped_block)
40
+ @created_records = strategy.new_records([@klass])[@klass]
41
+
42
+ return false unless @created_records.count == 1
43
+
44
+ record = @created_records.first
45
+
46
+ @attribute_mismatches = []
47
+ @attributes&.each do |field, value|
48
+ unless values_match?(value, record.public_send(field))
49
+ @attribute_mismatches << [field, value, record.public_send(field)]
50
+ end
51
+ end
52
+
53
+ if @attribute_mismatches.none? && @which_block
54
+ begin
55
+ @which_block.call(record)
56
+ rescue RSpec::Expectations::ExpectationNotMetError => e
57
+ @which_failure = e
58
+ end
59
+ end
60
+
61
+ if @attribute_mismatches.none? && @which_matcher && !@which_matcher.matches?(record)
62
+ @matcher_failure = @which_matcher.failure_message
63
+ end
64
+
65
+ if @should_return_record && @attribute_mismatches.empty? && @which_failure.nil? && @matcher_failure.nil? &&
66
+ !values_match?(record, @block_return_value)
67
+ @return_value_mismatch = { expected: record, actual: @block_return_value }
68
+ end
69
+
70
+ @attribute_mismatches.empty? && @which_failure.nil? && @matcher_failure.nil? && @return_value_mismatch.nil?
71
+ end
72
+
73
+ def failure_message
74
+ if @created_records.count != 1
75
+ "the block should have created 1 #{@klass}, but created #{@created_records.count}"
76
+ elsif @attribute_mismatches&.any?
77
+ @attribute_mismatches.map do |field, expected, actual|
78
+ expected_description = composable_matcher?(expected) ? expected.description : expected.inspect
79
+ "Expected #{field.inspect} to be #{expected_description}, but was #{actual.inspect}"
80
+ end.join("\n")
81
+ elsif @which_failure
82
+ @which_failure.message
83
+ elsif @return_value_mismatch
84
+ expected = @return_value_mismatch[:expected]
85
+ actual = @return_value_mismatch[:actual]
86
+ "Expected the block to return the created #{@klass}, but it returned #{actual.inspect} instead of #{expected.inspect}"
87
+ else
88
+ @matcher_failure
89
+ end
90
+ end
91
+
92
+ def failure_message_when_negated
93
+ if @created_records.count == 1 && @attributes && @attribute_mismatches.none?
94
+ "the block should not have created a #{@klass} with attributes #{format_attributes_hash(@attributes)}, but did"
95
+ elsif @created_records.count == 1 && @which_block && !@which_failure
96
+ "the newly created #{@klass} should have failed an expectation in the given block, but didn't"
97
+ elsif @created_records.count == 1
98
+ "the block should not have created a #{@klass}, but created #{@created_records.count}: #{@created_records.inspect}"
99
+ else
100
+ "the block created a #{@klass} that matched all given criteria"
101
+ end
102
+ end
103
+
104
+ def description
105
+ "create a #{@klass}, optionally verifying attributes"
106
+ end
107
+
108
+ private
109
+
110
+ def composable_matcher?(value)
111
+ value.respond_to?(:failure_message_when_negated)
112
+ end
113
+
114
+ def format_attributes_hash(attributes)
115
+ hash = attributes.transform_values { |value| format_value(value) }
116
+ ActiveRecordChangeMatchers.format_hash_for_message(hash)
117
+ end
118
+
119
+ def format_value(value)
120
+ composable_matcher?(value) ? value.description : value
121
+ end
122
+ end
123
+ end
124
+
125
+ RSpec::Matchers.define :create_a_new do |klass, *strategy_arg|
126
+ supports_block_expectations
127
+ options = strategy_arg.first.is_a?(Hash) ? strategy_arg.first : {}
128
+ base = ActiveRecordChangeMatchers::CreateANewMatcher.new(klass, options)
129
+
130
+ match { |block| base.matches?(block) }
131
+ failure_message { base.failure_message }
132
+ failure_message_when_negated { base.failure_message_when_negated }
133
+ description { base.description }
134
+
135
+ chain(:with_attributes) { |*args| base.with_attributes(*args) }
136
+ chain(:which) { |&block| base.which(&block) }
137
+ chain(:which_is_expected_to) { |*args| base.which_is_expected_to(*args) }
138
+ chain(:and_return_it) { base.and_return_it }
139
+ end
140
+
141
+ RSpec::Matchers.alias_matcher :create_a, :create_a_new
142
+ RSpec::Matchers.alias_matcher :create_an, :create_a_new
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordChangeMatchers
4
+ class CreateAssociatedMatcher
5
+ include ActiveSupport::Inflector
6
+ include RSpec::Matchers::Composable
7
+
8
+ def supports_block_expectations?
9
+ true
10
+ end
11
+
12
+ def initialize(scope_or_counts)
13
+ @scope_or_counts = scope_or_counts
14
+ end
15
+
16
+ def with_attributes(attributes)
17
+ @expected_attributes = normalize_expected_attributes(attributes)
18
+ validate_attributes_count!
19
+ self
20
+ end
21
+
22
+ def which(&block)
23
+ @which_block = block
24
+ self
25
+ end
26
+
27
+ def and_return_it
28
+ raise ArgumentError, '`and_return_it` only applies when expecting exactly one record' unless single_record?
29
+
30
+ @should_return_records = true
31
+ self
32
+ end
33
+
34
+ def and_return_them
35
+ @should_return_records = true
36
+ self
37
+ end
38
+
39
+ def matches?(block)
40
+ @block_return_value = nil
41
+ wrapped_block = proc { @block_return_value = block.call }
42
+ strategy = ActiveRecordChangeMatchers::Strategies.for_key(@strategy_key).new(wrapped_block)
43
+ @new_records = strategy.new_records(scopes)
44
+
45
+ @incorrect_counts = @new_records.each_with_object({}) do |(scope, records), incorrect|
46
+ expected_count = scope_counts[scope]
47
+ actual_count = records.size
48
+ incorrect[scope] = { expected: expected_count, actual: actual_count } if actual_count != expected_count
49
+ end
50
+
51
+ return false if @incorrect_counts.any?
52
+
53
+ if @expected_attributes
54
+ return false unless match_expected_attributes
55
+ end
56
+
57
+ run_which_block
58
+ check_return_value if @should_return_records
59
+
60
+ @which_failure.nil? && @return_value_mismatch.nil?
61
+ end
62
+
63
+ def failure_message
64
+ if @incorrect_counts&.any?
65
+ @incorrect_counts.map do |scope, counts|
66
+ "The block should have created #{count_str(scope.klass, counts[:expected])} within the scope, but created #{counts[:actual]}."
67
+ end.join(' ')
68
+ elsif @incorrect_attributes&.any? { |_, list| list.any? }
69
+ format_attributes_failure_message
70
+ elsif @which_failure
71
+ @which_failure.message
72
+ elsif @return_value_mismatch
73
+ format_return_value_failure_message
74
+ else
75
+ 'Unknown error'
76
+ end
77
+ end
78
+
79
+ def failure_message_when_negated
80
+ scope_counts.map do |scope, expected_count|
81
+ "The block should not have created #{count_str(scope.klass, expected_count)} within the scope, but created #{expected_count}."
82
+ end.join(' ')
83
+ end
84
+
85
+ def description
86
+ counts_strs = scope_counts.map { |scope, count| count_str(scope.klass, count) }
87
+ "create #{counts_strs.join(', ')} within the given scope(s)"
88
+ end
89
+
90
+ private
91
+
92
+ def scope_counts
93
+ @scope_counts ||= normalize_scope_counts(@scope_or_counts)
94
+ end
95
+
96
+ def normalize_scope_counts(value)
97
+ case value
98
+ when Hash
99
+ value
100
+ else
101
+ { value => 1 }
102
+ end
103
+ end
104
+
105
+ def scopes
106
+ scope_counts.keys
107
+ end
108
+
109
+ def single_scope?
110
+ scope_counts.size == 1
111
+ end
112
+
113
+ def single_record?
114
+ single_scope? && scope_counts.values.first == 1
115
+ end
116
+
117
+ def count_str(klass, count)
118
+ "#{count} #{klass.name.pluralize(count)}"
119
+ end
120
+
121
+ def normalize_expected_attributes(attributes)
122
+ return {} if attributes.nil?
123
+
124
+ if single_scope? && attributes.is_a?(Hash) && attributes.keys.all? { |k| k.is_a?(Symbol) || k.is_a?(String) }
125
+ { scopes.first => [attributes] }
126
+ elsif single_scope? && attributes.is_a?(Array)
127
+ { scopes.first => attributes }
128
+ else
129
+ attributes
130
+ end
131
+ end
132
+
133
+ def validate_attributes_count!
134
+ return unless @expected_attributes
135
+
136
+ @expected_attributes.each do |scope, hashes|
137
+ expected_count = scope_counts[scope]
138
+ next unless expected_count && hashes.size != expected_count
139
+
140
+ raise ArgumentError,
141
+ "Specified the block should create #{expected_count} #{scope.klass.name} within the scope, but provided #{hashes.size} attribute specifications"
142
+ end
143
+ end
144
+
145
+ def match_expected_attributes
146
+ @matched_records = Hash.new { |h, k| h[k] = [] }
147
+ @all_attributes = Hash.new { |h, k| h[k] = [] }
148
+ @incorrect_attributes = @expected_attributes.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(scope, expected_attrs_list), incorrect|
149
+ records = @new_records[scope]
150
+ @all_attributes[scope] = expected_attrs_list.map(&:keys).flatten.uniq
151
+ expected_attrs_list.each do |expected_attrs|
152
+ matched = (records - @matched_records[scope]).find do |record|
153
+ expected_attrs.all? { |k, v| values_match?(v, record.public_send(k)) }
154
+ end
155
+ if matched
156
+ @matched_records[scope] << matched
157
+ else
158
+ incorrect[scope] << expected_attrs
159
+ end
160
+ end
161
+ end
162
+ @unmatched_records = scopes.to_h do |scope|
163
+ [scope, @new_records[scope] - @matched_records[scope]]
164
+ end.reject { |_, records| records.empty? }
165
+ @incorrect_attributes.none? { |_, list| list.any? }
166
+ end
167
+
168
+ def run_which_block
169
+ @which_failure = nil
170
+ return unless @which_block
171
+
172
+ new_records_by_klass = @new_records.transform_keys(&:klass)
173
+ @which_block.call(new_records_by_klass)
174
+ rescue RSpec::Expectations::ExpectationNotMetError => e
175
+ @which_failure = e
176
+ end
177
+
178
+ def check_return_value
179
+ return unless @incorrect_counts.empty? &&
180
+ (@incorrect_attributes.nil? || @incorrect_attributes.none? { |_, l| l.any? }) &&
181
+ @which_failure.nil?
182
+
183
+ all_created = @new_records.values.flatten
184
+ returned = Array(@block_return_value)
185
+ missing = all_created.reject { |r| returned.any? { |o| values_match?(r, o) } }
186
+ if single_record? && missing.any?
187
+ @return_value_mismatch = { expected: all_created.first, actual: @block_return_value }
188
+ elsif missing.any?
189
+ @return_value_mismatch = { expected: all_created, actual: @block_return_value, missing: missing }
190
+ end
191
+ end
192
+
193
+ def format_attributes_failure_message
194
+ "The block should have created:\n" +
195
+ @expected_attributes.map do |scope, attrs|
196
+ " #{attrs.count} #{scope.klass.name} with these attributes:\n" +
197
+ attrs.map { |a| " #{ActiveRecordChangeMatchers.format_hash_for_message(a)}" }.join("\n")
198
+ end.join("\n") +
199
+ "\nDiff:" +
200
+ @incorrect_attributes.map do |scope, attrs|
201
+ next if attrs.empty?
202
+
203
+ "\n Missing #{attrs.count} #{scope.klass.name} with these attributes:\n" +
204
+ attrs.map { |a| " #{ActiveRecordChangeMatchers.format_hash_for_message(a)}" }.join("\n")
205
+ end.compact.join("\n") +
206
+ @unmatched_records.map do |scope, records|
207
+ "\n Extra #{records.count} #{scope.klass.name} with these attributes:\n" +
208
+ records.map do |r|
209
+ attrs = @all_attributes[scope].each_with_object({}) { |attr, h| h[attr] = r.public_send(attr) }
210
+ " #{ActiveRecordChangeMatchers.format_hash_for_message(attrs)}"
211
+ end.join("\n")
212
+ end.join("\n")
213
+ end
214
+
215
+ def format_return_value_failure_message
216
+ if single_record?
217
+ "Expected the block to return the created #{scopes.first.klass.name}, but it returned #{@return_value_mismatch[:actual].inspect} instead of #{@return_value_mismatch[:expected].inspect}"
218
+ else
219
+ missing = @return_value_mismatch[:missing]
220
+ 'Expected the block to return the created records, but it did not return all of them. ' \
221
+ "Missing records: #{missing.map(&:inspect).join(', ')}. " \
222
+ "Expected all of: #{@return_value_mismatch[:expected].map(&:inspect).join(', ')}, " \
223
+ "but got: #{@return_value_mismatch[:actual].inspect}"
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ RSpec::Matchers.define :create_associated do |scope_or_counts|
230
+ supports_block_expectations
231
+ base = ActiveRecordChangeMatchers::CreateAssociatedMatcher.new(scope_or_counts)
232
+
233
+ match { |block| base.matches?(block) }
234
+ failure_message { base.failure_message }
235
+ failure_message_when_negated { base.failure_message_when_negated }
236
+ description { base.description }
237
+
238
+ chain(:with_attributes) { |*args| base.with_attributes(*args) }
239
+ chain(:which) { |&block| base.which(&block) }
240
+ chain(:and_return_it) { base.and_return_it }
241
+ chain(:and_return_them) { base.and_return_them }
242
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordChangeMatchers
4
+ class CreateRecordsMatcher
5
+ include ActiveSupport::Inflector
6
+ include RSpec::Matchers::Composable
7
+
8
+ def supports_block_expectations?
9
+ true
10
+ end
11
+
12
+ def initialize(record_counts, options = {})
13
+ @record_counts = record_counts
14
+ @strategy_key = options[:strategy]
15
+ end
16
+
17
+ def with_attributes(attributes)
18
+ if (mismatch = attributes.find { |klass, hashes| hashes.size != @record_counts[klass] })
19
+ mismatched_class, hashes = mismatch
20
+ raise ArgumentError,
21
+ "Specified the block should create #{@record_counts[mismatched_class]} #{mismatched_class}, but provided #{hashes.size} #{mismatched_class} attribute specifications"
22
+ end
23
+ @expected_attributes = attributes
24
+ self
25
+ end
26
+
27
+ def which(&block)
28
+ @which_block = block
29
+ self
30
+ end
31
+
32
+ def and_return_them
33
+ @should_return_records = true
34
+ self
35
+ end
36
+
37
+ def matches?(block)
38
+ @block_return_value = nil
39
+ wrapped_block = proc { @block_return_value = block.call }
40
+ strategy = ActiveRecordChangeMatchers::Strategies.for_key(@strategy_key).new(wrapped_block)
41
+ @new_records = strategy.new_records(@record_counts.keys)
42
+
43
+ @incorrect_counts = @new_records.each_with_object({}) do |(klass, new_records), incorrect|
44
+ actual_count = new_records.count
45
+ expected_count = @record_counts[klass]
46
+ incorrect[klass] = { expected: expected_count, actual: actual_count } if actual_count != expected_count
47
+ end
48
+
49
+ return false if @incorrect_counts.any?
50
+
51
+ if @expected_attributes
52
+ return false unless match_expected_attributes
53
+ end
54
+
55
+ run_which_block
56
+ check_return_value if @should_return_records
57
+
58
+ @which_failure.nil? && @return_value_mismatch.nil?
59
+ end
60
+
61
+ def failure_message
62
+ if @incorrect_counts&.any?
63
+ @incorrect_counts.map do |klass, counts|
64
+ "The block should have created #{count_str(klass, counts[:expected])}, but created #{counts[:actual]}."
65
+ end.join(' ')
66
+ elsif @incorrect_attributes&.any?
67
+ format_attributes_failure_message
68
+ elsif @which_failure
69
+ @which_failure.message
70
+ elsif @return_value_mismatch
71
+ format_return_value_failure_message
72
+ else
73
+ 'Unknown error'
74
+ end
75
+ end
76
+
77
+ def failure_message_when_negated
78
+ @record_counts.map do |klass, expected_count|
79
+ "The block should not have created #{count_str(klass, expected_count)}, but created #{expected_count}."
80
+ end.join(' ')
81
+ end
82
+
83
+ def description
84
+ counts_strs = @record_counts.map { |klass, count| count_str(klass, count) }
85
+ "create #{counts_strs.join(', ')}"
86
+ end
87
+
88
+ private
89
+
90
+ def count_str(klass, count)
91
+ "#{count} #{klass.name.pluralize(count)}"
92
+ end
93
+
94
+ def match_expected_attributes
95
+ @matched_records = Hash.new { |hash, key| hash[key] = [] }
96
+ @all_attributes = Hash.new { |hash, key| hash[key] = [] }
97
+ @incorrect_attributes = @expected_attributes.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(klass, expected_attributes), incorrect|
98
+ @all_attributes[klass] = expected_attributes.map(&:keys).flatten.uniq
99
+ expected_attributes.each do |expected_attrs|
100
+ matched_record = (@new_records.fetch(klass) - @matched_records[klass]).find do |record|
101
+ expected_attrs.all? { |k, v| values_match?(v, record.public_send(k)) }
102
+ end
103
+ if matched_record
104
+ @matched_records[klass] << matched_record
105
+ else
106
+ incorrect[klass] << expected_attrs
107
+ end
108
+ end
109
+ end
110
+ @unmatched_records = @matched_records.map do |klass, records|
111
+ [klass, @new_records[klass] - records]
112
+ end.to_h.reject { |_k, v| v.empty? }
113
+ @incorrect_attributes.none?
114
+ end
115
+
116
+ def run_which_block
117
+ @which_failure = nil
118
+ return unless @which_block
119
+
120
+ @which_block.call(@new_records)
121
+ rescue RSpec::Expectations::ExpectationNotMetError => e
122
+ @which_failure = e
123
+ end
124
+
125
+ def check_return_value
126
+ return unless @incorrect_counts.empty? &&
127
+ (@incorrect_attributes.nil? || @incorrect_attributes.none?) &&
128
+ @which_failure.nil?
129
+
130
+ all_created_records = @new_records.values.flatten
131
+ return_value_array = Array(@block_return_value)
132
+ missing_records = all_created_records.reject do |record|
133
+ return_value_array.any? { |returned| values_match?(record, returned) }
134
+ end
135
+ return unless missing_records.any?
136
+
137
+ @return_value_mismatch = {
138
+ expected: all_created_records,
139
+ actual: @block_return_value,
140
+ missing: missing_records
141
+ }
142
+ end
143
+
144
+ def format_attributes_failure_message
145
+ "The block should have created:\n" +
146
+ @expected_attributes.map do |klass, attrs|
147
+ " #{attrs.count} #{klass} with these attributes:\n" +
148
+ attrs.map { |a| " #{ActiveRecordChangeMatchers.format_hash_for_message(a)}" }.join("\n")
149
+ end.join("\n") +
150
+ "\nDiff:" +
151
+ @incorrect_attributes.map do |klass, attrs|
152
+ "\n Missing #{attrs.count} #{klass} with these attributes:\n" +
153
+ attrs.map { |a| " #{ActiveRecordChangeMatchers.format_hash_for_message(a)}" }.join("\n")
154
+ end.join("\n") +
155
+ @unmatched_records.map do |klass, records|
156
+ "\n Extra #{records.count} #{klass} with these attributes:\n" +
157
+ records.map do |r|
158
+ attrs = @all_attributes[klass].each_with_object({}) { |attr, h| h[attr] = r.public_send(attr) }
159
+ " #{ActiveRecordChangeMatchers.format_hash_for_message(attrs)}"
160
+ end.join("\n")
161
+ end.join("\n")
162
+ end
163
+
164
+ def format_return_value_failure_message
165
+ missing = @return_value_mismatch[:missing]
166
+ expected = @return_value_mismatch[:expected]
167
+ actual = @return_value_mismatch[:actual]
168
+ 'Expected the block to return the created records, but it did not return all of them. ' \
169
+ "Missing records: #{missing.map(&:inspect).join(', ')}. " \
170
+ "Expected all of: #{expected.map(&:inspect).join(', ')}, " \
171
+ "but got: #{actual.inspect}"
172
+ end
173
+ end
174
+ end
175
+
176
+ RSpec::Matchers.define :create_records do |record_counts, *strategy_arg|
177
+ supports_block_expectations
178
+ options = strategy_arg.first.is_a?(Hash) ? strategy_arg.first : {}
179
+ base = ActiveRecordChangeMatchers::CreateRecordsMatcher.new(record_counts, options)
180
+
181
+ match { |block| base.matches?(block) }
182
+ failure_message { base.failure_message }
183
+ failure_message_when_negated { base.failure_message_when_negated }
184
+ description { base.description }
185
+
186
+ chain(:with_attributes) { |*args| base.with_attributes(*args) }
187
+ chain(:which) { |&block| base.which(&block) }
188
+ chain(:and_return_them) { base.and_return_them }
189
+ end
190
+
191
+ RSpec::Matchers.alias_matcher :create, :create_records
@@ -15,7 +15,7 @@ module ActiveRecordChangeMatchers
15
15
  block.call
16
16
 
17
17
  classes.each_with_object({}) do |klass, new_records|
18
- new_records[klass] = klass.where("#{column_name} > ?", time_before).to_a +
18
+ new_records[klass] = klass.where("#{column_name} > ?", time_before).to_a
19
19
  new_records[klass] += klass.where("#{column_name} = ?", time_before).where.not(klass.primary_key => existing_records[klass]).to_a
20
20
  end
21
21
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordChangeMatchers
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -1,6 +1,7 @@
1
1
  require "rspec/expectations"
2
2
  require "active_record"
3
3
 
4
+ require_relative "active_record_change_matchers/hash_format"
4
5
  Dir[File.dirname(__FILE__) + "/active_record_change_matchers/**/*.rb"].each {|file| require file }
5
6
 
6
7
  module ActiveRecordChangeMatchers