380
324

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

FactoryGirlのtransientとtraitを活用する

Posted at

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というクラスがemailencrypted_passwordを持っていて、それを何かの事情で別モデルに分けなければいけなくなった時などの場合です。

元々はUserクラスがその属性を持っていたため、各テストコードに以下の様なコードが沢山あるかもしれません。

FactoryGirl.create(:user, email: "joker1007@example.com")

この場合、カラムを消すとテストが盛大にぶっ壊れます。
そこで、とりあえずの移行策として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

380
324
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
380
324

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?