在测试中嘲弄和剔除

时间:2014-07-16 14:05:08

标签: ruby-on-rails ruby testing rspec tdd

我最近学会了如何在rspec中存根,并发现它的一些好处是我们可以解耦代码(例如控制器和模型),更有效的测试执行(例如,存根数据库调用)。

但是我认为如果我们存根,代码可以与特定的实现紧密相关,因此牺牲了我们稍后重构代码的方式。

示例:

UsersController

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

控制器规范

# /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

通过这样做,我只是将实现限制为仅使用User.create?所以稍后如果我更改代码,我的测试将失败,即使两个代码的目的相同,即将新用户保存到数据库

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

然而,如果我在没有存根的情况下测试控制器,我可以创建一个真实的记录,然后检查数据库中的记录。只要控制器能够像这样保存用户

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

如果代码看起来不正确或有错误,真的很抱歉,我没有检查代码,但你明白了我的意思。

所以我的问题是,我们可以模拟/存根而不必绑定到特定的实现吗?如果是的话,请你在rspec中给我一个例子

3 个答案:

答案 0 :(得分:1)

您应该使用模拟和存根来模拟服务外部到它使用的代码,但您对测试中运行它们不感兴趣。

例如,假设您的代码使用的是twitter gem

status = client.status(my_client)

在您的测试中,您确实希望您的代码转到Twitter API并获得虚假客户的身份!而是存根该方法:

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

现在,您可以安全地检查您的代码,确定性,短期运行结果!

这是一个用例,其中存根和模拟是有用的,还有更多。当然,像任何其他工具一样,它们可能会被滥用,并在以后引起疼痛。

答案 1 :(得分:0)

内部创建调用save和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

所以你的第二次测试可能会涵盖两种情况。

编写与实现无关的测试并不简单。这就是为什么集成测试具有很多价值,并且比单元测试更适合测试应用程序的行为。

答案 2 :(得分:0)

在您呈现的代码中,您并不是完全嘲笑或抄袭。我们来看看第一个规范:

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

在这里,您正在测试用户是否收到了“创建”消息。你是对的,这个测试有问题,因为如果你改变控制器'create'动作的实现,它就会破坏,这会破坏测试的目的。测试应该灵活变化,而不是阻碍。

您要做的不是测试实现,而是副作用。什么是控制器'创建'动作应该做什么?它应该创建一个用户。这是我测试它的方式

# /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

至于嘲弄和咒语,我试图远离过多的顽固。我认为当你试图测试条件时它非常有用。这是一个例子:

# /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

这里我正在剔除'保存'并返回'假',以便我可以测试如果用户无法保存将会发生什么。

此外,其他答案是正确的,说您想要存根外部服务,这样每次运行测试套件时都不会调用他们的API。