FactoryGirlでテストデータを定義する時に、transientとtraitを活用すると色々捗るという話。
transientは実際に作成するデータと直接関係無い新しいattributeを定義する機能。
そこで定義されたものは実際のmodelにはセットされないしattributes_forでも出力されません。
何のために使うかというと作成時に挙動を変更するためのフラグや追加データとして利用するのが一般的です。
traitは属性の定義を一纏めにして名前を付けられる機能です。
parentを指定したfactoryの継承とは違い、traitは単体ではfactoryとして機能しません。
あるfactoryの特定の状態に名前を付けて、付け外しできるようにする、というのが主な使い方になります。
例えば、あるfactoryをある時はadminある時は非adminで作りたい時等に有効です。
個人的には継承を使うよりtraitを使う方がテストデータの作成には便利なケースが多いのではないかと思います。
継承は親の属性が全て入ってしまうので、パターンの組み合わせが複雑になってくると調整が難しくなります。
一方で、一つの実体と見做して問題無いバリエーションの場合は継承の方が向いていると思います。
ちなみに、factoryは全てのfactoryの中で一意である必要がありますが、traitには名前空間が存在し、factory定義内で定義したtraitはそのfactoryでしか利用できません。
トップレベルで定義しておけば複数モデルで共有することもできます。polymorphic関連を作る時等に活用できると思います。
活用の具体例
transientの活用法についての具体例を示してみます。
transientは各attributeの値にも利用でき、callbackでも利用可能です。
factory :user_account do
transient do
domain "example.com"
end
email { "hoge@#{domain}" }
encrypted_password "abcdefg12345"
trait :admin do
admin true
end
trait :inactive do
inactive true
end
trait :with_login_histories do
transient do
login_count 5
end
after(:create) do |user_account, evaluator|
evaluator.login_count.times do
user_account.login_histories.create!
end
end
end
end
こんな感じで定義しておけば、ログイン履歴を任意の数だけ持つuser_account
を作るのも簡単です。
FactoryGirl.create(:user_account, :with_login_histories, login_count: 10)
という感じで呼び出せば10個のlogin_histories
を作ってくれます。
何も指定が無ければ5つ作成され、traitの指定が無ければ何も作成されません。
もし1度ログインしていてadmin権限を持っているけどアカウントが無効化されている、といったテストデータが欲しい時は以下の様に書きます。
FactoryGirl.create(:user_account, :admin, :inactive, :with_login_histories, login_count: 1)
この様にテストの状況次第で付け外しできる状態を定義したり、関連オブジェクトの作成状態を制御したい時にはtraitとtransientが有効です。
また、transientはDBスキーマ変更時にも役に立ちます。
元々はUser
というクラスがemail
とencrypted_password
を持っていて、それを何かの事情で別モデルに分けなければいけなくなった時などの場合です。
元々はUser
クラスがその属性を持っていたため、各テストコードに以下の様なコードが沢山あるかもしれません。
FactoryGirl.create(:user, email: "[email protected]")
この場合、カラムを消すとテストが盛大にぶっ壊れます。
そこで、とりあえずの移行策としてtransientを利用することもできます。
factory :user do
name "joker1007"
transient do
email nil
domain nil
encrypted_password nil
user_account_trait []
end
user_account do
attributes = {
email: email,
encrypted_password: encrypted_password,
domain: domain,
}.reject do |_, v|
v.nil?
end
FactoryGirl.create(:user_account, *user_account_trait, attributes)
end
trait :admin do
transient do
user_account_trait [:admin]
end
end
end
こんな感じで直接User
に保存できない属性をtransientで定義しておき、それを移行先のUserAccount
のfactoryに移譲します。
これにより、上記の古いままのテストデータ作成処理がそのまま動作するようになります。
また、関連先のtraitの呼び出しをtransient経由で制御する事もできます。
データの依存関係が複雑でfactoryを多段で呼び出して色々なデータを作らなければいけない時に、traitとそれを制御するためのtransientをセットしておく事で、細かくテストデータの作成処理を制御する事ができます。
実際の所、テストコードでは必要な最低限のテストデータだけ作るようにして、実装が変わったらちゃんと意図が分かるようにテストコードも直すべきだと思いますが、色々と大きくなってしまったアプリケーションだとそう簡単に行かないこともあります。歴史的経緯によって色々複雑に絡みあってたりしてそう簡単に弄れないとか……。
そういった場合にtransientを使ってfactoryの互換性を維持するテクニックは役に立つかもしれません。
実際に動作するサンプルコード全体はここに置いておきます。
https://gist.github.com/joker1007/328ecb38bee3801ab28f