Inconsistency between if + else and if -> unless

capybara, railstutorial.org, rspec, ruby

Solution

This is correct behavior for RSpec, even though it seems unexpected.

Consider this code:

x = nil
"foo" if x.nil?
"bar" unless x.nil?
#=> 
"foo"
nil

The `...unless` statement returns `nil` when the condition is falsey.

In your custom matcher, the `...unless` statement 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.

Problem

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 :)

Original source