Reputation: 13195
I want to check for many changes in a model when submitting a form in a feature spec. For example, I want to make sure that the user name was changed from X to Y, and that the encrypted password was changed by any value.
I know there are some questions about that already, but I didn't find a fitting answer for me. The most accurate answer seems like the ChangeMultiple
matcher by Michael Johnston here: Is it possible for RSpec to expect change in two tables?. Its downside is that one only check for explicit changes from known values to known values.
I created some pseudo code on how I think a better matcher could look like:
expect {
click_button 'Save'
}.to change_multiple { @user.reload }.with_expectations(
name: {from: 'donald', to: 'gustav'},
updated_at: {by: 4},
great_field: {by_at_leaset: 23},
encrypted_password: true, # Must change
created_at: false, # Must not change
some_other_field: nil # Doesn't matter, but want to denote here that this field exists
)
I have also created the basic skeleton of the ChangeMultiple
matcher like this:
module RSpec
module Matchers
def change_multiple(receiver=nil, message=nil, &block)
BuiltIn::ChangeMultiple.new(receiver, message, &block)
end
module BuiltIn
class ChangeMultiple < Change
def with_expectations(expectations)
# What to do here? How do I add the expectations passed as argument?
end
end
end
end
end
But now I'm already getting this error:
Failure/Error: expect {
You must pass an argument rather than a block to use the provided matcher (nil), or the matcher must implement `supports_block_expectations?`.
# ./spec/features/user/registration/edit_spec.rb:20:in `block (2 levels) in <top (required)>'
# /Users/josh/.rvm/gems/ruby-2.1.0@base/gems/activesupport-4.2.0/lib/active_support/dependencies.rb:268:in `load'
# /Users/josh/.rvm/gems/ruby-2.1.0@base/gems/activesupport-4.2.0/lib/active_support/dependencies.rb:268:in `block in load'
Any help in creating this custom matcher is highly appreciated.
Upvotes: 137
Views: 74372
Reputation: 4823
BroiSatse's answer is the best, but if you are using RSpec 2 (or have more complex matchers like .should_not
), this method also works:
lambda {
lambda {
lambda {
lambda {
click_button 'Save'
@user.reload
}.should change {@user.name}.from('donald').to('gustav')
}.should change {@user.updated_at}.by(4)
}.should change {@user.great_field}.by_at_least(23)
}.should change {@user.encrypted_password}
Upvotes: 5
Reputation: 216
The accepted answer is not 100% correct since the full compound matcher support for change {}
has been added in RSpec version 3.1.0. If you try to run the code given in accepted answer with the RSpec version 3.0, you would get an error.
In order to use compound matchers with change {}
, there are two ways;
def supports_block_expectations?; true; end
into the RSpec::Matchers::BuiltIn::Compound
class, either by monkey patching it or directly editing the local copy of the gem. An important note: this way is not completely equivalent to the first one, the expect {}
block runs multiple times in this way!The pull request which added the full support of compound matchers functionality can be found here.
Upvotes: 2
Reputation: 44675
In RSpec 3 you can setup multiple conditions at once (so the single expectation rule is not broken). It would look sth like:
expect {
click_button 'Save'
@user.reload
}.to change { @user.name }.from('donald').to('gustav')
.and change { @user.updated_at }.by(4)
.and change { @user.great_field }.by_at_least(23}
.and change { @user.encrypted_password }
It is not a complete solution though - as far as my research went there is no easy way to do and_not
yet. I am also unsure about your last check (if it doesn't matter, why test it?). Naturally you should be able to wrap it within your custom matcher.
Upvotes: 290
Reputation: 1932
If you want to test that multiple records were not changed, you can invert a matcher using RSpec::Matchers.define_negated_matcher
. So, add
RSpec::Matchers.define_negated_matcher :not_change, :change
to the top of your file (or to your rails_helper.rb
) and then you can chain using and
:
expect{described_class.reorder}.to not_change{ruleset.reload.position}.
and not_change{simple_ruleset.reload.position}
Upvotes: 58