【问题标题】:Mocking and stubbing in testing测试中的模拟和存根
【发布时间】:2014-07-16 14:05:07
【问题描述】:

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

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

例子:

用户控制器

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

如果我在没有存根的情况下测试控制器,我可以创建一个真实的记录,然后检查数据库中的记录。只要控制器能够保存用户 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

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

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

【问题讨论】:

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


    【解决方案1】:

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

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

    status = client.status(my_client)
    

    在您的测试中,您真的不希望您的代码转到 twitter API 并获取您的虚假客户端的状态!相反,你存根该方法:

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

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

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

    【讨论】:

    • 那么你是说在我的例子中,最好不要模拟代码?
    • @verdy 是的,模拟不适合您的情况。有一些宝石和方法可以测试这些东西
    【解决方案2】:

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

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

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

    【讨论】:

    • 但是稍后在集成测试中,如果我模拟一些代码,它不是依赖于实现吗?所以它会再次完全取决于被模拟的方法?
    【解决方案3】:

    在您呈现的代码中,您并不完全是在模拟或存根。让我们看一下第一个规范:

    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
    

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

    您要做的不是测试实现,而是副作用。控制器“创建”操作应该做什么?它应该创建一个用户。这是我将如何测试它

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

    【讨论】:

    • 在您的“它保存新用户”示例中,这是否意味着我仍然需要调用数据库?换句话说,我们不能完全消除在测试中调用外部服务的需要吗?
    • 在这个例子中它仍然会命中数据库,我认为这会使测试慢一点。您可以取消对数据库的调用。但我发现控制器测试不应该是真正的单元测试。它们应该是功能测试。因此,您想从控制器访问数据库以确保它实际上将记录保存在此处。但是,如果您调用外部服务来保存记录,我会删除该外部服务。
    猜你喜欢
    • 1970-01-01
    • 2013-08-04
    • 1970-01-01
    • 1970-01-01
    • 2013-09-25
    • 2015-11-06
    • 1970-01-01
    • 1970-01-01
    • 2017-05-01
    相关资源
    最近更新 更多