对于这个模糊的标题感到抱歉,这个问题有很多可动的部分,所以我认为只有在看到我的代码后才会清楚。我非常确定我知道这里发生了什么,并且正在寻找有关如何以不同方式做到这一点的反馈:
我有一个User模型,通过ActiveRecord回调设置一个uuid attr(这实际上是在" SetsUuid"关注,但效果是这样的):
class User < ActiveRecord::Base
before_validation :set_uuid, on: :create
validates :uuid, presence: true, uniqueness: true
private
def set_uuid
self.uuid = SecureRandom.uuid
end
end
我正在为&#34; foo / add_user&#34;写一个功能性的rspec控制器测试。端点。控制器代码看起来像这样(还有一些其他的东西,比如错误处理,@ foo和@params由过滤器设置,但你明白了。我知道这一切都有效。)
class FoosController < ApplicationController
def add_user
@foo.users << User.find_by_uuid!(@params[:user_id])
render json: {
status: 'awesome controller great job'
}
end
end
我正在为案例编写一个功能性的rspec控制器测试&#34; foo / add_user将用户添加到foo&#34;。我的测试看起来大致如此(再次,留下这里的东西,但重点应该是明显的,我知道它都按预期工作。另外,只是为了抢先评论:不,我实际上并不是这样&#39;硬编码&#34;用户-uuid&#34;测试中的字符串值,这仅用于示例)
RSpec.describe FoosController, type: :controller do
describe '#add_user' do
it_behaves_like 'has @foo' do
it_behaves_like 'has @params', {user_id: 'user-uuid'} do
context 'user with uuid exists' do
let(:user) { create(:user_with_uuid, uuid: params[:user_id]) } # params is set by the 'has @params' shared_context
it 'adds user with uuid to @foo' do
route.call() # route is defined by a previous let that I truncated from this example code
expect(foo.users).to include(user) # foo is set by the 'has @foo' shared_context
end
end
end
end
end
end
这是我的用户工厂(我已尝试以几种不同的方式设置uuid,但我的问题(我在下面说)总是一样的。我认为这种方式(有特征)是最多的优雅,所以我在这里放的是:
FactoryGirl.define do
factory :user do
email { |n| "user-#{n}@example.com" }
first_name 'john'
last_name 'naglick'
phone '718-555-1234'
trait :with_uuid do
after(:create) do |user, eval|
user.update!(uuid: eval.uuid)
end
end
factory :user_with_uuid, traits: [:with_uuid]
end
end
最后,问题:
仅当我在规范中的route.call()之前引用user.uuid时才有效。
如果我只是添加了行&#34; user.uuid&#34;在route.call()之前,一切都按预期工作。
如果我没有该行,则规范失败,因为用户的uuid 实际上没有被工厂特征中的after(:create)回调更新,因而是User.find_by_uuid!控制器中的行找不到用户。
只是为了抢占另一条评论:我不会问如何重新编写此规范,以便它能够像我想要的那样工作。我已经知道了无数的方法(最简单,最明显的是在规范中手动更新user.uuid而忘记在工厂中完全设置uuid。我在这里问的是为什么工厂女孩会这样做?
我知道它与懒惰属性有关(显而易见的是,如果我有一条评估user.uuid的行,它会神奇地起作用),但为什么呢?并且,甚至更好:是否有某种方式我可以做我想要的(在工厂设置uuid)并让一切都像我想要的那样工作?我觉得它对rspec / factorygirl的使用相当优雅,所以我真的很喜欢这样工作。
感谢您阅读我的长篇问题!非常感谢任何见解
答案 0 :(得分:2)
您的问题与FactoryGirl关系不大,而且与let
懒惰评估有关。
使用let来定义memoized帮助器方法。该值将缓存 多个调用在同一个示例中,但不是跨越示例。
请注意,let是惰性计算的:直到第一次才会计算它 调用它定义的方法。你可以用let!强制方法的 在每个例子之前调用。
由于您的测试没有调用user
对象,直到期望没有创建任何内容。要强制rspec加载对象,可以使用let!
。
答案 1 :(得分:0)
您应该使用before_validation
,而不是使用after_initialize
回调。这样,即使在调用.valid?
in the model lifecycle之前,也会触发回调。
class User < ActiveRecord::Base
before_initialization :set_uuid!, on: :create, if: :set_uuid?
validates :uuid, presence: true, uniqueness: true
private
def set_uuid!
# we should also check that the UUID
# does not actually previously exist in the DB
begin
self.uuid = SecureRandom.uuid
end while User.where(uuid: self.uuid).any?
end
def set_uuid?
self.uuid.nil?
end
end
虽然用SecureRandom.uuid
两次生成相同哈希值的可能性非常小,但由于鸽笼原理,这是可能的。如果你在运气不好的彩票中获得最大限度,那么这只会产生一个新的UUID。
由于回调在验证发生之前触发,因此这里的实际逻辑应完全包含在模型中。因此,无需在FactoryGirl中设置回调。
相反,你会设置你的规范:
let!(:user) { create(:user) }
it 'adds user with uuid to @foo' do
post :add_user, user_id: user.uuid, { baz: 3 }
end