Test for "lambda {.}.should change(x, y).by(z)" failing?

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

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

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