How to use mocks correctly?
I have this class:
class EnablePost
def initialize(post_klass, id)
raise "oops" if post_klass.blank?
@post_klass = post_klass
@id = id
end
def perform
post = @post_klass.find_by_id(@id)
return unless post
post.update_attribute :enabled, true
end
end
The spec I have to write to check the above:
describe EnablePost do
it "should enable a post" do
post = mock
post.should_receive(:blank?).and_return(false)
post.should_receive(:find_by_id).with(22).and_return(post)
post.should_receive(:update_attribute).with(:enabled, true)
result = EnablePost.new(Post, 22).perform
result.should be_true
end
end
But I really want to do it EnablePost
like a black box. I don't want to mock :blank?
, :find_by_id
or :update_attribute
. That is, I want my spec to look like this:
describe EnablePost do
it "should enable a post" do
post = mock
result = EnablePost.new(post, 22).perform
result.should be_true
end
end
What am I missing here? Am I using mocks incorrectly?
Yes, you are confusing ridicule and stubs.
Good explanation: http://jamesmead.org/talks/2007-07-09-introduction-to-mock-objects-in-ruby-at-lrug/
Mocks:
- Different things for different people
- Ambiguous terminology
- Confusion with Rails "mocks"
Object layout:
- Expected method calls set in advance
- Checks for valid calls that match expected
Also check http://martinfowler.com/articles/mocksArentStubs.html [thanks to Zombies user in the comments]
If you are using RSpec, these are aliases double, mock and stub. RSpec expects you to choose any method name that makes your code as clear as possible.
Your first piece of test code is using the word "mock" correctly. You set up the method calls you expect to be called in advance, and then you make them.
However, you are testing two different areas of your code: the first area is the initialization method, the second is the #perform method.
You may find it easier to cheat and stub if you write smaller methods:
# What you want to test here is the raise and the member variables.
# You will stub the post_klass.
def initialize(post_klass, post_id) # post_id is a better name
raise "oops" if post_klass.blank?
@post_klass = post_klass
@post_id = post_id # because we don't want to mask Object#id
end
attr_accessor :post_id
attr_accessor :post_klass
# What you want to test here is the post_klass calls #find_by_id with post_id.
# See we've changed from using instance variables to methods.
def post
post_klass.find_by_id(post_id)
end
# What you want to test here is if the update happens.
# To test this, stub the #post method.
def perform
p = post
return unless p
p.update_attribute :enabled, true
end
When you write your code this way, you can easily stub the #post method.
See this for an example RSpec example showing the difference between mock and stub:
http://blog.firsthand.ca/2011/12/example-using-rspec-double-mock-and.html