Mocking in rspec
With code, mocking is quite straight forward in Rails.
for e.g. We have a @user
object and we want some method foo
on it to always return 5
expect(@user).to receive(:foo).and_return(5)
This is a partial double. We have a user
object and its partially mocked. every other method, attribute etc. in it works like normal. This means:
puts @user.id # returns the id of the user
Partial Doubles are different from instance doubles, class doubles and object doubles. These 3 methods create an empty double object. They do no have any original functionality and can not call original. What they do have is that they do not allow mocking of things which do not exist on the item whose template they were created from. For e.g.
user = instance_double("User")
expect(user).to receive(:non_existant_method).and_return(5)
# throws RSpec::Mocks::MockExpectationError: User does not implement: non_existant_method
This is because user does not have the method called non_existant_method
Difference between instance_double
and class_double
is that one uses the instance as the template and the other uses the class as the template. They are both built by passing in the name of the Class like:
u1 = instance_double("User")
u2 = class_double("User")
The object_double
on the other hand takes in an object as parameter and uses that as the template. For e.g.
u = User.first
o1 = object_double(u)
o2 = object_double(User)
Object doubles have a unique advantage over instance_doubles because they have access to method_missing and dynamically created methods from it. So those are part of the skelton and can therefore be stubbed.
We also have just double
. These create a strict object which does not follow any template and allows anything expecified and rejects the rest. They can be made loose too by specigying as_null_object
on them. This makes them return self on anything not specified. For e.g.
dbl = double
allow(dbl).to receive(:foo).and_return "5"
dbl.foo #5
dbl.sandeep #error
dbl1 = double("yo yo double", :say => "yo")
dbl1.say #yo
dbl1.sandeep #error
dbl2 = double("foo" :name => "foo").as_null_object
dbl2.name #foo
dbl2.sandeep # dbl2
So far partial_double
is my favorite. It allows to keep the sanity of the object and at the same time stub out things.
My second favorite is just a loose double
. Its very verstile. I can built it with no effort, it doesn’t check or complain on anything. I can use it pass in wherever an object is expected and then check for things happening in on that object. I can also use it completly remove dependecies.
For e.g. Lets say we need a user object which has 10 visitors with an average height of 6’ 0’’. It has a method called average_visitor_height
and that should return 6.
u = double("A cool usser", :average_height => 6).as_null_object
A common pattern used when writing tests is: Given-when-then AKA Act-Arrange-Assert. For E.g.
it "returns number of children when asked for size" do
# Given
u = FactoryGirl.create :user
child1 = FactoryGirl.create :child, :user_id => u.id
child2 = FactoryGirl.create :child, :user_id => u.id
# When
num_of_children = u.children.size
# Then
expect(num_of_children).to eq(2)
end
This pattern breaks when checking for something to have been called. For e.g.
it "calls send_email after_building csv file" do
# Given
u = FactoryGirl.create :user
# Expect
expect(u).to receive(:send_email).at_least(:once)
# Then
u.build_csv
end
Note that in the above example we could not use the Given-When-Then pattern. We are setting up an expectation of something before we have even did the thing which make that something happen.
To assist in this we have what we call spies
. Spies allow us to monitor an object and then later check for message by using methods like have_received
. For e.g.
it "calls send_email after_building csv file" do
# Given
u = FactoryGirl.create :user
u1 = spy(u)
# Then
u1.build_csv
# Expect
expect(u1).to have_received(:send_email).at_least(:once)
end
Spying also works on partial_doubles. Here is an example:
it "calls send_email after_building csv file" do
# Given
u = FactoryGirl.create :user
allow(u).to receive(:send_email)
# Then
u1.build_csv
# Expect
expect(u1).to have_received(:send_email).at_least(:once)
end