Can you simultaneously test a state change and a return value in RSpec?
Asked Answered
T

4

6

Say I have a method MyKlass#do_thing that I want to call exactly once in a test (because it might change a state), and that should return true on successful state change and false otherwise. I want to write a spec that looks something like this:

it "Updates myvalue if condition is met" do
  wojit = MyKlass.new

  # ... assuming condition is met
  expect { wojit.do_thing }.to change { wojit.value }.and.be true
end

But this particular approach gets an ArgumentError, because #and expects 1 argument.

I can make it work with the following abomination:

expect { expect(wojit.do_thing).to be true }.to change { wojit.value }

But that is hiiiiideous. Am I missing something more idiomatic?

Tricia answered 13/3, 2018 at 12:43 Comment(0)
T
2

This question is 6y old but still relevant, so I wanted to update with latest preferred solution for this problem. subject evaluation is cached. This allows you first check the change effect, and on the next line check return value without it running twice. Basically same as @rampion suggested just more readable.

subject(:do_thing) { wojit.do_thing }

let(:wojit) { MyKlass.new }

it "changes and returns true" do
  expect{ do_thing }.to change(wojit, :value)
  expect( do_thing ).to be(true)
end
Tabithatablature answered 16/7, 2024 at 11:0 Comment(1)
Nice! Thanks :)Tricia
E
6

Another approach is just to stick the return value in a variable.

return_value = nil
expect{ return_value = wojit.do_thing }.to change{ wojit.value }
expect( return_value ).to be true

YMMV as to whether it's better or worse than nested expects.

Egestion answered 12/10, 2018 at 19:16 Comment(1)
I wish there was better syntax than this. Guilty of writing this code as well. Sorta wish expect { } would return the result of the block so you could do return_value = expect { blah }.to change... but that could be confusing too. Not sure I like more complicated approaches like shimming in functionality/matchers to support this. Jury's definitely out on whether nested expects are better/worse... 🤷‍♂️Ibbison
P
2

Maybe is not what you are looking for, but I actually think that "something more idiomatic" would be to make to tests using a describe or context block to express better that you testing the same case.

describe "When condition is met" do
  it "updates the value" do
    wojit = Wojit.new
    expect { wojit.do_thing }.to change { wojit.value }
  end

  it "returns true" do
    wojit = Wojit.new
    expect(wojit.do_thing).to be_true
  end
end
Pasha answered 14/3, 2018 at 3:17 Comment(1)
+1 Each test should ideally only check one thing, so splitting up into multiple tests is usually the idiomatic approach.Urinate
T
2

This question is 6y old but still relevant, so I wanted to update with latest preferred solution for this problem. subject evaluation is cached. This allows you first check the change effect, and on the next line check return value without it running twice. Basically same as @rampion suggested just more readable.

subject(:do_thing) { wojit.do_thing }

let(:wojit) { MyKlass.new }

it "changes and returns true" do
  expect{ do_thing }.to change(wojit, :value)
  expect( do_thing ).to be(true)
end
Tabithatablature answered 16/7, 2024 at 11:0 Comment(1)
Nice! Thanks :)Tricia
P
0

You could implement your own custom Matcher for this specific case like:

RSpec::Matchers.define :respond_with do |expected| 
  match do |actual|
    actual.call == expected
  end
  # allow the matcher to support block expectations
  supports_block_expectations
  # make sure this executes in the correct context
  def expects_call_stack_jump?
    true
  end
end

Then your expectation would be something like

it "Updates myvalue if condition is met" do
  wojit = MyKlass.new
  expect{wojit.do_thing}.to change(wojit, :value).and(respond_with(true))
end

The key here is that be,eq, etc. does not support block expectations and thus cannot be used in conjuction with expect{...} so we implemented an equality matcher that does support block expectations (supports_block_expectations? #=> true) and jumped it up the stack (this is very important in this case otherwise the change block creates a conflicting actual *Not sure I 100% understand why but trust me it does).

In this case actual will be the block body (as a Proc) so we just have to call it to compare the result to the expected value.

You could however abstract this out further to something like

RSpec::Matchers.define :have_response do |expectation| 

  supports_block_expectations

  def expects_call_stack_jump?
    true
  end
  #Actual matching logic 
  match do |actual|
     @actual_value = actual.respond_to?(:call) ? actual.call : actual
    expect(@actual_value).to(expectation)
  end

  failure_message do |actual|
    "expected response to be #{expectation.expected} but response was #{@actual_value}"
  end
  failure_message_when_negated do |actual|
    "expected response not to be #{expectation.expected} but response was #{@actual_value}"
  end

end

#define negation for chaining purposes as needed
RSpec::Matchers.define_negated_matcher :not_have_response, :have_response

Which would allow you to use all the methods that do not support block expectations like so

it "Updates myvalue if condition is met" do
  wojit = MyKlass.new
  expect{wojit.do_thing}.to change(wojit, :value).and(have_response(be true))
  # or 
  # expect{wojit.do_thing}.to not_have_response(be false).and(change(wojit, :value))
end

Only issue with either one of these approaches is that the block will be called once for the change and once for the response check so depending on your circumstances this could cause issues.

Presentational answered 13/3, 2018 at 15:36 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.