-
-
Save sj26/1388023 to your computer and use it in GitHub Desktop.
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 |
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)) }
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?
I just found the assign_to shoulda matcher. :-(
Admittedly it doesn't give as much flexibility in matching the assigned values...
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.
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) }
:)
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!
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.
Motivation
This is ugly:
Instead:
It's more like a subject modifier, a la
its
, but inline and allowing nicely described specs:And presents nice diffs when it fails:
I'm not sold on
to
but I couldn't think of something that fitsshould
/should_not
andbe
/be_a
/etc, plusexpect
sets a precedent ofto
.I also don't like that it feels a bit WET, but I couldn't use these bits nicely out of rspec.