2016-03-30 6 views
1

Entschuldigung für den vagen Titel, es gibt viele bewegliche Teile zu diesem Problem, so denke ich, dass es erst klar wird, nachdem ich meinen Code gesehen habe. Ich bin ziemlich sicher, dass ich weiß, was hier vor sich geht und bin auf der Suche nach Feedback darüber, wie es anders zu machen:FactoryGirl-Attribut, das nach (: create) gesetzt wurde, bleibt bestehen, bis es referenziert wird?

ich ein User-Modell haben, das eine UUID attr über ein Active Rückruf setzt (das ist tatsächlich in einem " SetsUuid“Besorgnis, aber die Wirkung ist dies):

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 

ich bin einen funktionellen rspec Controller Test für ein Schreiben von "foo/add_user" Endpunkt. Der Controller Code sieht wie folgt aus (es gibt einige andere Sachen wie Fehlerbehandlung und @foo und @params durch Filter gesetzt werden, aber Sie erhalten den Punkt. Ich weiß, das ist alles funktioniert.)

class FoosController < ApplicationController 
    def add_user 
     @foo.users << User.find_by_uuid!(@params[:user_id]) 
     render json: { 
      status: 'awesome controller great job' 
     } 
    end 
end 

Ich schreibe ein funktionaler rspec controller test für den fall "foo/add_user fügt user zu foo hinzu". Mein Test sieht in etwa so aus (auch hier sind Dinge hier draußen, aber der Punkt sollte offensichtlich sein, und ich weiß, dass alles wie beabsichtigt funktioniert. Außerdem, um die Kommentare vorwegzunehmen: Nein, ich bin nicht wirklich "hartcodieren" den "Benutzer -uuid“String-Wert im Test, ist dies nur für das Beispiel)

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 

Und hier ist mein Benutzer Fabrik (ich habe versucht, die uUID auf verschiedene Weise einstellen, aber mein Problem (das ich in unten) ist immer die gleiche ich denke, auf diese Weise (mit Merkmalen) die eleganteste ist, so was ist das ich hier setzen).

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 

Schließlich das Problem:

Dies funktioniert nur, wenn ich vor route.call() in der Spezifikation user.uuid referenziere.

Wie in, wenn ich einfach die Zeile "user.uuid" vor route.call() hinzufügen, funktioniert alles wie vorgesehen.

Wenn ich nicht diese Zeile haben, schlägt die Spezifikation, weil die UUID des Benutzers tatsächlich nicht durch die nach aktualisiert werden (: create) Rückruf in der Eigenschaft in der Fabrik und damit die User.find_by_uuid! Zeile im Controller findet den Benutzer nicht.

Und nur um einen anderen Kommentar vorwegzunehmen: Ich frage nicht, wie man diese Spezifikation neu schreiben, so dass es funktioniert, wie ich will. Ich weiß schon eine Vielzahl von Möglichkeiten, um dies (die einfachste und offensichtlichste ist manuell zu aktualisieren user.uuid in der Spezifikation selbst und vergessen Sie die UUID in der Fabrik insgesamt über die Einstellung) zu tun. Die Frage, die ich hier stelle, ist Warum verhält sich Factorygirl so?

Ich weiß, dass es etwas mit Lazy-Attributen zu tun hat (offensichtlich durch die Tatsache, dass es magisch funktioniert, wenn ich eine Zeile habe, die user.uuid auswertet), aber warum? Und noch besser: ist es eine Möglichkeit, die ich tun kann, was ich hier will (die UUID in der Werkseinstellung) und hat alles funktioniert, wie ich die Absicht? Ich denke, es ist eine ziemlich elegant aussehende Verwendung von rspec/factorygirl, also würde ich wirklich gerne so arbeiten.

Dank für meine lange Frage beim Lesen! Sehr schätzt nicht nur die Einsicht

+1

es hier nicht Fabrik Mädchen, es ist rspec des 'let' - wenn Sie rufen' 'lassen es zwingt das Objekt erstellt werden/anhielt. – Anthony

+0

@Anthony wow, danke! Das behebt mein Problem. Könntest du deine Antwort als echte Antwort posten, damit ich sie annehmen kann? Ich habe nicht darüber nachgedacht, wie Dinge in let() nicht aufgerufen werden, bis sie referenziert sind. So arbeitete ich unter dem falschen Eindruck, dass create() das Objekt immer sofort erstellen würde, obwohl es in einem let() ist. Der Grund, warum mein Code nicht mit let() funktioniert, liegt darin, dass das Benutzerobjekt erst nach der route.call() erstellt wird, zu welchem ​​Zeitpunkt die Controller-Aktion bereits ausgeführt wurde, also find_by_uuid! findet immer nichts. –

Antwort

2

Ihr Problem weniger mit factory zu tun hat und mehr zu tun mit let lazily ausgewertet.

From the docs:

Verwenden lassen eine memoized Hilfsmethode zu definieren. Der Wert wird zwischen mehreren Aufrufen im selben Beispiel, aber nicht in verschiedenen Beispielen zwischengespeichert.

Beachten Sie, dass let ist lazy-evaluated: Es wird erst beim ersten Mal die Methode ausgewertet, die es definiert ist aufgerufen. Sie können verwenden lassen! um den Aufruf der Methode vor jedem Beispiel zu erzwingen.

Da Ihr Test ruft nicht das user Objekt, bis der Erwartung, es ist nichts geschaffen. Um rspec zum Laden des Objekts zu zwingen, können Sie let! verwenden.

0

Anstatt den Rückruf before_validation zu verwenden, sollten Sie after_initialize verwenden. Auf diese Weise wird der Rückruf ausgelöst, noch bevor .valid?in the model lifecycle heißt.

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 

Obwohl die Chance auf den gleichen Hash zweimal mit SecureRandom.uuid Erzeugung extrem schlank ist es aufgrund der pigeonhole prinzipiell möglich ist. Wenn Sie in der Pechlotterie ausgereizt sind, würde dies einfach eine neue UUID generieren.

Da der Callback vor der Validierung ausgelöst wird, sollte die tatsächliche Logik hier vollständig im Modell enthalten sein. Daher muss in FactoryGirl kein Rückruf eingerichtet werden.

Stattdessen würden Sie Setup Ihre spec wie so:

let!(:user) { create(:user) } 

it 'adds user with uuid to @foo' do 
    post :add_user, user_id: user.uuid, { baz: 3 } 
end 
+0

Danke für die Kommentare. Ich wollte damit umgehen, die geringe Wahrscheinlichkeit zu berücksichtigen, dass ein wiederholter Uuid erzeugt wird, und ich mag es wirklich, wie Sie das mit dem Beginn-Ende-While kodierten (wusste nicht einmal, dass die Syntax funktionierte, tbqh). –

Verwandte Themen