Forum: RSpec test for "lambda {.}.should change(x, y).by(z)" failing?

Posted by Patrick Collins (patrick99e99)
on 2011-11-23 23:10
(Received via mailing list)
I wrote a test that looked like this:

  it "increases the user's reputation" do
    lambda { @comment.update_attribute(:helpful, true) }.should 
change(@seller.reload, 
:reputation_score).by(Event.reputation_change_for(:mark_helpful))
  end

And I am getting this error:
  1) Comment comments on posts marking a comment as helpful increases 
the user's reputation
     Failure/Error: lambda { @comment.update_attribute(:helpful, true) 
}.should change(@seller.reload, 
:reputation_score).by(Event.reputation_change_for(:mark_helpful))
       reputation_score should have been changed by 3, but was changed 
by 0

--

The way the actual code works is, I have a comment observer that does:

  def after_update(comment)
    Event.create_for_user(comment.user, :mark_helpful)
  end

And event.rb does something like:

  def create_for_user(user, event_type)
    create!(:user => user, :reputation_change => 
Event::SCORES[event_type])
  end

The user model has an before_save callback which does:

  def sum_points
    self.reputation_score = events.sum(:reputation_change)
    self.points = events.sum(:points_change)
  end


---

Anyway, so this test fails, and I am not sure why...  If I write it in a
slightly less-cool way:

  it "increases the user's reputation" do
    @seller.reputation_score.should == 0
    @comment.update_attribute(:helpful, true)
    @seller.reload.reputation_score.should == 
Event.reputation_change_for(:mark_helpful)
  end

Then it passes...  If I throw in a debugger statement in there and 
manually
call the code, the reputation_score does indeed increase......  So I am
confused why the lambda {}.change thing isn't working?

Thanks.

Patrick J. Collins
http://collinatorstudios.com
Posted by David Chelimsky (Guest)
on 2011-11-24 21:35
(Received via mailing list)
On Nov 23, 2011, at 3:33 PM, Patrick J. Collins wrote:

> I wrote a test that looked like this:
>
>   it "increases the user's reputation" do
>     lambda { @comment.update_attribute(:helpful, true) }.should 
change(@seller.reload, 
:reputation_score).by(Event.reputation_change_for(:mark_helpful))

The change matcher has several forms, including:

  lambda { ... }.should change(object, method).by(amount)
  lambda { ... }.should change { object.method }.by(amount)

Your example uses the former, which results in the following (roughly):

  receiver = @seller.reload
  value_before = receiver.reputation_score
  { @comment.update_attribute(:helpful, true) }.call
  value_after = receiver.reputation_score
  (value_after - value_before).should 
eq(Event.reputation_change_for(:mark_helpful))

As you can see, @seller.reload is only evaluated once, and its 
reputation score is going to be the same both times. If you want 
@seller.reload eval'd before and after, then you have to use the block 
form:

  lambda { @comment.update_attribute(:helpful, true) }.
    should change {@seller.reload.reputation_score }.
    by(Event.reputation_change_for(:mark_helpful))

Tangent: this is testing two things - @seller.reputation_score and 
Event.reputation_change_for(:mark_helpful). If either is failing to work 
correctly, this example won't tell you which. I'd recommend sticking to 
literals in expectations:

  lambda { @comment.update_attribute(:helpful, true) }.
    should change {@seller.reload.reputation_score }.by(3)

HTH,
David
Posted by Patrick Collins (patrick99e99)
on 2011-11-24 23:21
(Received via mailing list)
> As you can see, @seller.reload is only evaluated once, and its reputation
> score is going to be the same both times.

Aha..  Makes perfect sense.  Thanks.

> Tangent: this is testing two things - @seller.reputation_score and
> Event.reputation_change_for(:mark_helpful). If either is failing to work
> correctly, this example won't tell you which. I'd recommend sticking to
> literals in expectations:
>
>   lambda { @comment.update_attribute(:helpful, true) }.  should change
>   {@seller.reload.reputation_score }.by(3)

Hmmm..  I totally get why you say this, but part of me really hates the 
idea of
that 3 someday changing to 5 and then my test breaking, forcing me to 
update
the 3 in multiple places.

Would it not be safe to assume that if there's a test for Event.rb 
verifying
the behavior of Event.reputation_change_for, then it's safe to use that 
in a
example?  When I originally wrote this, I wanted to stub out the 
Event::SCORES
constant and return a hash with just something like:
  { :mark_helpful => { :reputation_change => 123 } }

But I couldn't figure out how to stub a constant.....  Of course I could 
just
make my test overwrite the mark helpful value of that constant like:

Event:SCORES[:mark_helpful] = { :reputation_change => 123 }

But that felt a little wrong so I chose to just rely on a class 
convenience
method to return the constant's value.

Any thoughts on these other approaches?

Patrick J. Collins
http://collinatorstudios.com
Please log in before posting. Registration is free and takes only a minute.
Existing account (Switch to SSL-encrypted connection)
NEW: Do you have a Google/GoogleMail or Yahoo account? No registration required!
Log in with Google account | Log in with Yahoo account
No account? Register here.