RSpec: Expect to change multiple

Ruby on-RailsRspecMatcher

Ruby on-Rails Problem Overview


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.

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

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.

Solution 2 - Ruby on-Rails

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}

Solution 3 - Ruby on-Rails

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}

Solution 4 - Ruby on-Rails

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;

  • First one is, you have to have at least RSpec version 3.1.0.
  • Second one is, you have to add 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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJoshua MuheimView Question on Stackoverflow
Solution 1 - Ruby on-RailsBroiSatseView Answer on Stackoverflow
Solution 2 - Ruby on-RailsMatthew HineaView Answer on Stackoverflow
Solution 3 - Ruby on-RailsZack MorrisView Answer on Stackoverflow
Solution 4 - Ruby on-RailsFoo Bar ZooView Answer on Stackoverflow