在(:create)之后设置的FactoryGirl属性不会持续直到被引用?

时间:2016-03-30 18:42:18

标签: ruby-on-rails ruby rspec factory-bot

对于这个模糊的标题感到抱歉,这个问题有很多可动的部分,所以我认为只有在看到我的代码后才会清楚。我非常确定我知道这里发生了什么,并且正在寻找有关如何以不同方式做到这一点的反馈:

我有一个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的使用相当优雅,所以我真的很喜欢这样工作。

感谢您阅读我的长篇问题!非常感谢任何见解

2 个答案:

答案 0 :(得分:2)

您的问题与FactoryGirl关系不大,而且与let懒惰评估有关。

From the docs

  

使用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