Skip to content

Instantly share code, notes, and snippets.

@sj26
Created November 23, 2011 06:27
Show Gist options
  • Save sj26/1388023 to your computer and use it in GitHub Desktop.
Save sj26/1388023 to your computer and use it in GitHub Desktop.
Rspec rails assign matcher
module RSpec::Rails::Matchers::Assign
private
# +nodoc+
class AssignMatcher
attr_accessor :actual, :operator, :expected
def initialize scope, name, expected=nil
@scope = scope
@name = name
@actual = nil
@operator = :==
@expected = [expected]
end
def description
"assign @#{@name}#{" to #{@operator} #{@expected.first.inspect}" if @expected.first}"
end
def failure_message_for_should
if @expected.first.nil?
"expected to assign @#{name}"
elsif ['==','===', '=~'].include?(operator)
"expected: #{expected.first.inspect}\n got: #{actual.inspect} (using #{operator})"
else
"expected: #{operator} #{expected.first.inspect}\n got: #{operator.gsub(/./, ' ')} #{actual.inspect}"
end
end
def failure_message_for_should_not
if @expected.first.nil?
"expected not to assign @#{name}"
else
"expected not: #{operator} #{expected.first.inspect}\n got: #{operator.gsub(/./, ' ')} #{actual.inspect}"
end
end
def diffable?
true
end
def to matcher_or_expected=nil
# Operator matchers, we need to get tricky!
if matcher_or_expected.nil?
AssignOperator.new self
# Meta-matching
elsif matcher_or_expected.respond_to? :matches?
AssignMetaMatcher.new @scope, @name, matcher_or_expected
# Just a value, set expected
else
@expected = [matcher_or_expected]
self
end
end
def matches? actual
# We discard `actual`!
@actual = @scope.assigns[@name]
if @expected.first.nil?
@actual.present?
else
@actual.send @operator, @expected.first
end
end
end
# +nodoc+
class AssignOperator
def initialize matcher
@matcher = matcher
end
['==', '===', '=~', '>', '>=', '<', '<='].each do |operator|
define_method operator do |expected|
@matcher.operator = operator
@matcher.expected = [expected]
@matcher
end
end
end
# +nodoc+
class AssignMetaMatcher
def initialize scope, name, matcher
@scope = scope
@name = name
@matcher = matcher
end
def matches? actual
# We discard `actual`!
@actual = @scope.assigns[@name]
@matcher.matches? @actual
end
def description
"assign @#{@name} to #{@matcher.description}"
end
def method_missing name, *args, &block
@matcher.send name, *args, &block
end
end
public
# Lets you write:
#
# subject { get :show }
# it { should assign(:blah) }
# it { should assign(:blah).to("something") }
#
# or, more interestingly:
#
# it { should assign(:blah).to == "something" }
# it { should assign(:blah).to be_a String }
# it { should assign(:blah).to satisfy { |value| Thing.exists? :blah => value } }
#
def assign *args, &block
AssignMatcher.new self, *args, &block
end
end
RSpec::Rails::ControllerExampleGroup.send :include, RSpec::Rails::Matchers::Assign
@sj26
Copy link
Author

sj26 commented Nov 23, 2011

Motivation

This is ugly:

subject { get :show }
specify { subject and assigns[:blah].should be_present }
specify { subject and assigns[:blah].should be_a String }
specify { subject and assigns[:blah].should == "something" }

Instead:

subject { get :show }
it { should assign(:blah) }
it { should assign(:blah).to == "something" }
it { should assign(:blah).to be_a String }

It's more like a subject modifier, a la its, but inline and allowing nicely described specs:

MyController
  #show
    should assign @blah
    should assign @blah to == "something"
    should assign @blah to be a kind of String

And presents nice diffs when it fails:

  1) MyController#show assign @blah to == "something"
     Failure/Error: it { should assign(:blah).to == "something" }
       expected #<String:2533082340> => "something"
            got #<String:2532866800> => "thing"

I'm not sold on to but I couldn't think of something that fits should/should_not and be/be_a/etc, plus expect sets a precedent of to.

I also don't like that it feels a bit WET, but I couldn't use these bits nicely out of rspec.

@dchelimsky
Copy link

What about:

subject { get :show }
it { should assign(:blah) }
it { should assign(:blah => "something") }
it { should assign(:blah => /ometh/) }
it { should assign(:blah => instance_of(String)) }

@sj26
Copy link
Author

sj26 commented Nov 24, 2011

I like that, but what happens if I supply multiple assignments—how do descriptions cope? Or can you only supply one?

It means we couldn't use operators consistently

it { should assign(:blah) > "nothing" }
it { should assign(:blah) =~ /ometh/ }
it { should assign(:blah => instance_of(String)) }

and breaks the englishness of

it { should assign :blah => be_blank }

Should it be an alternative, not a replacement?

@sj26
Copy link
Author

sj26 commented Nov 24, 2011

I just found the assign_to shoulda matcher. :-(

Admittedly it doesn't give as much flexibility in matching the assigned values...

@dchelimsky
Copy link

matchers often respond to ==, so they can be used as mock arg constraints: foo.should_receive(:bar).with(instance_of(String)). That would be the idea here: if it's a regexp, use =~, otherwise use ==. Look back at my suggestion w/ that in mind. It's infinitely flexible because you can write any matchers you want with no additional syntax.

@dchelimsky
Copy link

re: the shoulda matcher, that feels a bit wordy to me. In fact, if we're using assign and to together, I would think it { should assign("this value").to(:this_variable_name) } :)

@sj26
Copy link
Author

sj26 commented Nov 25, 2011

Hash rockets make sense given the == override. should assign(:blah => include(:a => "b")) feels reasonable with this in mind. I was thinking in terms of subject modification rather than deep matching which doesn't really make sense.

Will refactor for great justice!

@sj26
Copy link
Author

sj26 commented Nov 26, 2011

I've popped this into a gem with specs for now: http://github.com/sj26/rspec-rails-assign

First commit is the old style, latest commit is with hash rocket syntax.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment