Patrick J. Collins
2011-Nov-23 21:33 UTC
[rspec-users] 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
David Chelimsky
2011-Nov-24 18:21 UTC
[rspec-users] test for "lambda {...}.should change(x, y).by(z)" failing.. ?
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> 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
Patrick J. Collins
2011-Nov-24 20:50 UTC
[rspec-users] test for "lambda {...}.should change(x, y).by(z)" failing.. ?
> 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