我最近学会了如何在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中给我一个例子
答案 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。