42

RSpec expect change:

it "should increment the count" do
  expect{Foo.bar}.to change{Counter.count}.by 1
end

Is there a way to expect change in two tables?

expect{Foo.bar}.to change{Counter.count}.by 1 
and change{AnotherCounter.count}.by 1 
B Seven
  • 44,484
  • 66
  • 240
  • 385

8 Answers8

79

I prefer this syntax (rspec 3 or later):

it "should increment the counters" do
  expect { Foo.bar }.to change { Counter,        :count }.by(1).and \
                        change { AnotherCounter, :count }.by(1)
end

Yes, this are two assertions in one place, but because the block is executed just one time, it can speedup the tests.

EDIT: Added Backslash after the .and to avoid syntax error

Georg Ledermann
  • 2,712
  • 4
  • 31
  • 35
  • This only tests the last assertion while ignoring all previous ones. See my answer for the proper way of combining assertions. – somecto Jun 19 '14 at 20:48
  • 1
    In Rspec 3, the above method will work if you use the composition operator `.and` (but you can't use the single `&` alias as described in the rspec docs or the && operator as in the original answer). I have edited the answer to work as intended. – Michael Johnston Jul 05 '14 at 23:27
  • 3
    actually, using rspec 3 composition results in the block being run multiple times :(. Sigh. Thank you Cargo Cultists for insisting on condemning everyone to integration tests that take orders of magnitude longer to run than they need to. – Michael Johnston Jul 05 '14 at 23:41
  • see [my answer](http://stackoverflow.com/a/24591809/173542) for a working change_multiple matcher for rspec 3 – Michael Johnston Jul 06 '14 at 01:00
  • 3
    @MichaelJohnston I've just checked compound matchers and it run block only once. Can you provide an example when block being run multiple times? – freemanoid Feb 21 '15 at 09:41
  • @freemanoid it's possible this has been fixed/changed. At the time I was encountering this issue, the above syntax was running the block for each matcher, and I had to write my own custom matcher (its in another answer on the page) – Michael Johnston Feb 22 '15 at 13:36
  • 1
    I get `ArgumentError: wrong number of arguments (0 for 1)` error if I use this syntax. – abhishek77in Aug 13 '15 at 11:37
  • @abhishek77in me too... `RSpec 3.0.4` here – caesarsol Dec 16 '15 at 20:07
  • 4
    @abhishek77in and @caesarsol: the `and` needs to be on the other line or you need to have the whole thing be on one line. – graygilmore Jan 20 '16 at 20:58
  • 1
    This works nicely and reads well, but you need to put a backslash after `.and` to continue the line – Neil Aug 05 '16 at 06:56
  • One benefit of this is to avoid costly setup (especially if you're using factory girl with a heavy model). – Jason Axelson May 23 '17 at 21:31
  • Documentation of `and` method: https://relishapp.com/rspec/rspec-expectations/v/3-6/docs/composing-matchers See "Composing matchers using a compound `and` expression" – Jared Beck Jun 12 '17 at 19:54
  • Is it possible to combine `and` with `not to` ? – Sergey Potapov Jul 04 '17 at 10:12
23

I got syntax errors trying to use @MichaelJohnston's solution; this is the form that finally worked for me:

it "should increment the counters" do
  expect { Foo.bar }.to change { Counter.count }.by(1)
    .and change { AnotherCounter.count }.by(1)
end

I should mention I'm using ruby 2.2.2p95 - I don't know if this version has some subtle change in parsing that causes me to get errors, it doesn't appear that anyone else in this thread has had that problem.

Community
  • 1
  • 1
Fred Willmore
  • 4,386
  • 1
  • 27
  • 36
  • Another thumbs-up here. Worked in latest version of everything in mid-2016. You can also pass params, instead of a block, to the `change` method like this: `to change(Counter, :count).by(1)` if that tickles your fancy. – Lucas Nelson Jun 11 '16 at 06:53
20

This should be two tests. RSpec best practices call for one assertion per test.

describe "#bar" do
  subject { lambda { Foo.bar } }

  it { should change { Counter.count }.by 1 }
  it { should change { AnotherCounter.count }.by 1 }
end
Chris Heald
  • 61,439
  • 10
  • 123
  • 137
  • 7
    Sometimes this leads to a LOT of boilerplate code (when a spec requires a complex setup). Or maybe I'm just doing it wrong :) – Sergio Tulentsev Nov 29 '12 at 00:25
  • 2
    Generally, if I'm doing a lot of boilerplate, I try to refine my context/describe blocks so I can do the setup in before blocks. That usually cleans it up. – Chris Heald Nov 29 '12 at 00:27
  • 3
    That's all fine for model/unit tests, but what about feature/integration tests, where it's normal to make many assertions in one test? – Arcolye Dec 26 '13 at 04:19
  • 5
    @Arcolye you have to just bite the bullet and have hours-long integration test runs, and if anybody complains about it tell them you can't change it because it would make the God of BDD angry and then the plane won't come. – Michael Johnston Jul 06 '14 at 01:14
  • 6
    "This should be two tests." except, of course, when it shouldn't. Like if it's an integration test, particularly of a consumer of an external service. Or if it is a test of atomicity or idempotency, and the separate side effects already have "proper" single assertion coverage and what is being tested is the atomicity/idempotency. – Michael Johnston Jul 06 '14 at 20:18
  • 1
    I think you missed the part in the provided link where the single-assertion rule applies pretty specifically to unit tests. If you're testing your database state in integration tests, you're doing it wrong. Grab your resultant state and run multiple assertions on it. Stop trying to jam that square peg into a round hole. – Chris Heald Jul 07 '14 at 22:04
  • This may be slightly out of date advice. RSpec itself includes aggregate_failures, which "allows multiple expectations in the provided block to fail, and then aggregates them into a single exception, rather than aborting on the first expectation failure like normal". http://www.rubydoc.info/github/rspec/rspec-expectations/RSpec%2FMatchers%3Aaggregate_failures – the911s Jan 24 '17 at 20:11
  • AFAIK, if you're wanting to write more than one assertion in an `it` block; move all the setup/execution code "out" into a `describe`/`context` section, and then have multiple `it` blocks within that. Although then there might be speed issues with running those setup blocks many times; but maybe `before all` might work instead of `before each`? – Narfanator Mar 03 '22 at 06:55
12

If you don't want to use the shorthand/context based approach suggested earlier, you can also do something like this but be warned it will run the expectation twice so it might not be appropriate for all tests.

it "should increment the count" do
  expectation = expect { Foo.bar }
  expectation.to change { Counter.count }.by 1
  expectation.to change { AnotherCounter.count }.by 1
end
rakvium
  • 64
  • 1
  • 7
Uri
  • 2,306
  • 2
  • 24
  • 25
5

Georg Ladermann's syntax is nicer but it doesn't work. The way to test for multiple value changes is by combining the values in arrays. Else, only the last change assertion will decide on the test.

Here is how I do it:

it "should increment the counters" do
  expect { Foo.bar }.to change { [Counter.count, AnotherCounter.count] }.by([1,1])
end

This works perfectecly with the '.to' function.

somecto
  • 239
  • 2
  • 9
  • 3
    unfortunately, this is not doing what you think it is. It is merely asserting that the final count is [1, 1]. To see what I mean, run your test with some Counter records already existing. – Michael Johnston Jul 05 '14 at 23:12
  • see [my answer](http://stackoverflow.com/a/24591809/173542) for a working change_multiple matcher for rspec 3 – Michael Johnston Jul 06 '14 at 01:01
  • @MichaelJohnston You're quite right. It doesn't work with the '.by' method, but it works with the '.to' method. So in fact it does answer the original question being if it's possible to expect changes in multiple tables. Plus, the block runs only once in this case. I agree that limiting tests to just one element is really suboptimal. – somecto Jul 11 '14 at 15:39
  • 4
    @somecto Your solution is close, but keep in mind the definition of Array subtraction: [1,1] - [] == [1,1], so this passes as long as the initial counts are both 0, but if the initial value is, for instance, [1,3], then your test subtracts [1,1] - [1,3] and the result is [], so it will fail. However, there is a data structure that defines subtraction the way you are using it: `Vector`. Try this: `require 'matrix'` (at the top of the file). `expect { Foo.bar }.to change { Vector[Counter.count, AnotherCounter.count] }.by(Vector[1,1])` – Isaac Betesh Aug 01 '14 at 18:37
  • @IsaacBetesh That's why I prefer to use 'change .to' instead of 'change .by'. It might be more verbose, require to check a value beforehand, but it always works. – somecto Aug 21 '14 at 14:16
  • @somecto I think most people are more familiar with your solution because 1) `Vector` is not included by default in a Ruby runtime environment, and 2) `Array` is more popular in tutorials. However, Vector is no more verbose--you only have to chain one method: `by`, instead of two: `from` and `to`. I actually thought of this solution when I had a situation where I wanted a spec to pass regardless of the presence of seed data in my database. `change{...}.from(...).to(...)` does not work in that case. What do you mean by `change .to` "always works"? – Isaac Betesh Aug 21 '14 at 18:50
4

The best way I've found is to do it "manually":

counters_before         = Counter.count
another_counters_before = AnotherCounter.count
Foo.bar
expect(Counter.count).to eq (counters_before + 1)
expect(AnotherCounter.count).to eq (another_counters_before + 1)

Not the most elegant solution but it works

GMA
  • 5,816
  • 6
  • 51
  • 80
3

After none of the proposed solutions proved to actually work, I accomplished this by adding a change_multiple matcher. This will only work for RSpec 3, and not 2.*

module RSpec
  module Matchers
    def change_multiple(receiver=nil, message=nil, &block)
      BuiltIn::ChangeMultiple.new(receiver, message, &block)
    end
    alias_matcher :a_block_changing_multiple,  :change_multiple
    alias_matcher :changing_multiple,          :change_multiple

    module BuiltIn
      class ChangeMultiple < Change
        private

          def initialize(receiver=nil, message=nil, &block)
            @change_details = ChangeMultipleDetails.new(receiver, message, &block)
          end
      end
      class ChangeMultipleDetails < ChangeDetails
        def actual_delta
          @actual_after = [@actual_after].flatten
          @actual_before = [@actual_before].flatten
          @actual_after.map.with_index{|v, i| v - @actual_before[i]}
        end
      end
    end
  end
end

example of usage:

it "expects multiple changes despite hordes of cargo cultists chanting aphorisms" do
  a = "." * 4
  b = "." * 10
  times_called = 0
  expect {
    times_called += 1
    a += ".."
    b += "-----"
  }.to change_multiple{[a.length, b.length]}.by([2,5])
  expect(times_called).to eq(1)
end

Making by_at_least and by_at_most work for change_multiple would require some additional work.

Martin Tournoij
  • 26,737
  • 24
  • 105
  • 146
Michael Johnston
  • 5,298
  • 1
  • 29
  • 37
  • This is nice, but it's not possible to mix assertions like `x` should change from 1 to 2, `y` should change to 3, `z` should change from 4, and k should simply change. – Joshua Muheim Apr 01 '15 at 09:45
  • Where did you find the docs/other to figure out how to write this? – Narfanator Mar 03 '22 at 07:03
2

I'm ignoring the best practices for two reasons:

  1. A set of my tests are regression tests, I want them to run fast, and they break rarely. The advantage of having clarity about exactly what is breaking isn't huge, and the slowdown of refactoring my code so that it runs the same event multiple times is material to me.
  2. I'm a bit lazy sometimes, and it's easier to not do that refactor

The way I'm doing this (when I need to do so) is to rely on the fact that my database starts empty, so I could then write:

foo.bar
expect(Counter.count).to eq(1)
expect(Anothercounter.count).to eq(1)

In some cases my database isn't empty, but I either know the before count, or I can explicitly test for the before count:

counter_before = Counter.count
another_counter_before = Anothercounter.count

foo.bar

expect(Counter.count - counter_before).to eq(1)
expect(Anothercounter.count - another_counter_before).to eq(1)

Finally, if you have a lot of objects to check (I sometimes do) you can do this as:

before_counts = {}
[Counter, Anothercounter].each do |classname|
  before_counts[classname.name] = classname.count
end

foo.bar

[Counter, Anothercounter].each do |classname|
  expect(classname.count - before_counts[classname.name]).to be > 0
end

If you have similar needs to me this will work, my only advice would be to do this with your eyes open - the other solutions proposed are more elegant but just have a couple of downsides in certain circumstances.

PaulL
  • 6,650
  • 3
  • 35
  • 39