I had an issue today with defining custom RSpec matchers that I resolved, but couldn’t actually see any reasoning behind why one of the approaches works and the other doesn’t, here’s the code:
Approach 1 — if + else:
RSpec::Matchers.define :have_success_message do |message|
match do |page|
if message.nil?
page.should have_selector('div.alert.alert-success')
else
page.should have_selector('div.alert.alert-success', text: message)
end
end
end
Approach 2 — if followed by unless
RSpec::Matchers.define :have_success_message do |message|
match do |page|
page.should have_selector('div.alert.alert-success') if message.nil?
page.should have_selector('div.alert.alert-success', text: message) unless message.nil?
end
end
I figure the first approach is better because it checks the condition only once, but nevertheless, the result should be the same, right?
Well, it turns out the tests with the first approach pass, while the tests with the second don’t. I am completely clueless as to why that is and would love if anyone could shed some light on that.
Edit:
Forgot to add the actual tests (with approach 2):
With the following HTML tag present:
<div class="alert alert-success">Profile updated</div>
I run 4 separate tests:
it { should have_success_message } # fails
it { should have_success_message('Profile updated') } # passes
it { should have_selector('div.alert.alert-success') } # passes
it { should have_selector('div.alert.alert-success', text: "Profile updated") } # passes
The failure is with following message:
1) User pages edit with valid information
Failure/Error: it { should have_success_message }
expected #<Capybara::Session> to have success message
# ./spec/requests/user_pages_spec.rb:80:in `block (5 levels) in <top (required)>'
When the HTML tag is not present, all 4 tests fail.
Edit 2:
I’ve tried another approach to verify whether control flow is correct:
Approach 3:
if message.nil?
puts "In if, message is: #{message.inspect}"
page.should(have_selector('div.alert.alert-success'))
end
unless message.nil?
puts "In unless, message is: #{message.inspect}"
page.should(have_selector('div.alert.alert-success', text: message))
end
With this approach the behavior is the same as with approach 2 – first test fails, following 3 pass.
Output is the following:
In if, message is: nil
In unless, message is: “Profile updated”
So the control flow looks alright, but
page.should(have_selector('div.alert.alert-success'))
fails, even though it passes outside the matcher. This truly is a mystery.
Final edit:
Just in response to the approved answer – when I switch the code like this:
page.should have_selector('div.alert.alert-success', text: message) unless message.nil?
page.should have_selector('div.alert.alert-success') if message.nil?
The tests look like this:
it { should have_success_message } # passes
it { should have_success_message('Profile updated') } # fails
it { should have_selector('div.alert.alert-success') } # passes
it { should have_selector('div.alert.alert-success', text: "Profile updated") } # passes
So I think that indeed the last line, when it’s not true, gets evaluated to nil, and that causes the whole mess. The first approach is better anyway, but I’m glad I have this problem out of my mind 🙂
This is correct behavior for RSpec, even though it seems unexpected.
Consider this code:
The
...unlessstatement returnsnilwhen the condition is falsey.In your custom matcher, the
...unlessstatement returns nil when your message is nil.That’s the last line in your match block, so your match block returns nil.
Then RSpec sees your match block return nil, which RSpec considers the same as false, so RSpec reports your custom matcher failed.