Agustinus Verdy
Agustinus Verdy

Reputation: 7305

Mocking and stubbing in testing

I've recently learned how to stub in rspec and found that some benefits of it are we can decouple the code (eg. controller and model), more efficient test execution (eg. stubbing database call).

However I figured that if we stub, the code can be tightly tied to a particular implementation which therefore sacrifice the way we refactor the code later.

Example:

UsersController

# /app/controllers/users_controller.rb
class UsersController < ApplicationController
  def create
    User.create(name: params[:name])
  end
end

Controller spec

# /spec/controllers/users_controller_spec.rb
RSpec.describe UsersController, :type => :controller do
  describe "POST 'create'" do
    it 'saves new user' do
      expect(User).to receive(:create)
      post :create, :name => "abc"
    end
  end
end

By doing that didn't I just limit the implementation to only using User.create? So later if I change the code my test will fail even though the purpose of both code is the same which is to save the new user to database

# /app/controllers/users_controller.rb
class UsersController < ApplicationController
  def create
    @user = User.new
    @user.name = params[:name]
    @user.save!
  end
end

Whereas if I test the controller without stubbing, I can create a real record and later check against the record in the database. As long as the controller is able to save the user Like so

RSpec.describe UsersController, :type => :controller do
  describe "POST 'create'" do
    it 'saves new user' do
      post :create, :name => "abc"
      user = User.first
      expect(user.name).to eql("abc")
    end
  end
end

Really sorry if the codes don't look right or have errors, I didn't check the code but you get my point.

So my question is, can we mock/stub without having to be tied to a particular implementation? If so, would you please throw me an example in rspec

Upvotes: 0

Views: 1045

Answers (3)

Freddy Rangel
Freddy Rangel

Reputation: 1363

In the code you're presented, you're not exactly mocking or stubbing. Let's take a look at the first spec:

RSpec.describe UsersController, :type => :controller do
  describe "POST 'create'" do
    it 'saves new user' do
      expect(User).to receive(:create)
      post :create, :name => "abc"
    end
  end
end

Here, you're testing that User received the 'create' message. You're right that there's something wrong with this test because it's going to break if you change the implementation of the controllers 'create' action, which defeats the purpose of testing. Tests should be flexible to change and not a hinderance.

What you want to do is not test implementation, but side effects. What is the controller 'create' action supposed to do? It's supposed to create a user. Here's how I would test it

# /spec/controllers/users_controller_spec.rb
RSpec.describe UsersController, :type => :controller do
  describe "POST 'create'" do
    it 'saves new user' do
      expect { post :create, name: 'abc' }.to change(User, :count).by(1)
    end
  end
end

As for mocking and stubbing, I try to stay away from too much stubbing. I think it's super useful when you're trying to test conditionals. Here's an example:

# /app/controllers/users_controller.rb
class UsersController < ApplicationController
  def create
    user = User.new(user_params)

    if user.save
      flash[:success] = 'User created'
      redirect_to root_path
    else
      flash[:error] = 'Something went wrong'
      render 'new'
  end
end

# /spec/controllers/users_controller_spec.rb
RSpec.describe UsersController, :type => :controller do
  describe "POST 'create'" do
    it "renders new if didn't save" do
      User.any_instance.stub(:save).and_return(false)
      post :create, name: 'abc'
      expect(response).to render_template('new')
    end
  end
end

Here I'm stubbing out 'save' and returning 'false' so I can test what's supposed to happen if the user fails to save.

Also, the other answers were correct in saying that you want to stub out external services so you don't call on their API every time you're running your test suite.

Upvotes: 0

Rafal
Rafal

Reputation: 2576

Internally create calls save and new

def create(attributes = nil, options = {}, &block)
  if attributes.is_a?(Array)
    attributes.collect { |attr| create(attr, options, &block) }
  else
    object = new(attributes, options, &block)
    object.save
    object
  end
end

So possibly your second test would cover both cases.

It is not straight forward to write tests which are implementation independent. That's why integration tests have a lot of value and are better suited than unit tests for testing the behavior of the application.

Upvotes: 0

Uri Agassi
Uri Agassi

Reputation: 37419

You should use mocking and stubbing to simulate services external to the code, which it uses, but you are not interested in them running in your test.

For example, say your code is using the twitter gem:

status = client.status(my_client)

In your test, you don't really want your code to go to twitter API and get your bogus client's status! Instead you stub that method:

expect(client).to receive(:status).with(my_client).and_return("this is my status!")

Now you can safely check your code, with deterministic, short running results!

This is one use case where stubs and mocks are useful, there are more. Of course, like any other tool, they may be abused, and cause pain later on.

Upvotes: 1

Related Questions