Ruby
Rails
FactoryBot

【翻訳】factory_bot 4.11で非推奨になった静的属性(static attributes)

TL;DR(この記事のざっくりまとめ)

  • factory_botの静的な属性は昔から混乱の原因になっており、開発チームのサポートの手間を増やしていた。
  • この混乱をなくすため、factory_bot 4.11で静的な属性を非推奨とし、バージョン5で完全に削除する。
  • 新しいファクトリ定義では常に動的な属性(ブロックを伴う属性定義)を使う必要がある。
  • rubocop-rspec gemをインストールして以下のコマンドを実行すれば、一括で静的な属性を動的な属性に書き換えることができる。
rubocop \
  --require rubocop-rspec \
  --only FactoryBot/AttributeDefinedStatically \
  --auto-correct

はじめに

2018年8月16日にリリースされたfactory_bot 4.11から、ファクトリの定義で静的な属性(static attributes)を使用していると警告が出るようになりました。

ファクトリ定義の静的な属性とはたとえば次のようなファクトリ定義です。

factory :robot do
  name "Ralph"
end

こういった定義があると、以下のような警告が出力されます。

DEPRECATION WARNING: Static attributes will be removed in FactoryBot 5.0. Please use dynamic
attributes instead by wrapping the attribute value in a block:

name { "Ralph" }

To automatically update from static attributes to dynamic ones,
install rubocop-rspec and run:

rubocop \
  --require rubocop-rspec \
  --only FactoryBot/AttributeDefinedStatically \
  --auto-correct

警告をなくすためには以下のように、ファクトリ定義を動的な属性を使う形式に書き換える必要があります。

factory :robot do
  name { "Ralph" }
end

この変更点について、factory_botの公式ブログで仕様を変更するに至った理由と、スムーズな移行方法について詳しく説明されています。

Deprecating static attributes in factory_bot 4.11

この記事では上記ブログの内容を翻訳します。

翻訳方針について

僕の翻訳では原文の意味を壊さない程度に意訳しています。
もし、まったく意味が違っていたり、おかしな内容があったりした場合は、コメント欄や編集リクエストで優しく指摘してやってください。

翻訳: factory_bot 4.11で非推奨になった静的属性(static attributes)

原文: Deprecating static attributes in factory_bot 4.11

factory_botは以前からブロックを使った動的(dynamically)な属性の定義ができるようになっています。

factory :robot do
  name { "Ralph" }
end

もしくは、次のように静的(statically)に定義することもできます。

factory :robot do
  name "Ralph"
end

動的な記法を使うと、新しいrobotオブジェクトを作るたびに、毎回新しく"Ralph"という文字列が作成されます。一方、静的な記法では毎回同一の文字列(つまり、メモリ上の同一オブジェクト)がセットされます。

長年にわたって、静的な属性(static attributes)は混乱を巻き起こす原因になってきました。GitHubやStack Overflowでは、この混乱に関連するissueや質問が延々と続いています。そのため、我々は2013年から静的な属性を非推奨にしようと考え始めました。

すべてのrobotオブジェクトの名前が同一の文字列になってしまうのはよろしくありません。なぜなら、文字列を破壊的に変更すると、他のrobotオブジェクトにもその変更が波及してしまうからです。

factory :robot do
  name "Ralph"
end

robot1 = build(:robot)
robot2 = build(:robot)

robot2.name
#=> "Ralph"

robot1.name << "y"

robot2.name
#=> "Ralphy"

これは混乱の原因になります。つまり、「テストの実行順序に依存する不具合」が発生してしまうのです。

Time.nowのようなメソッドと静的な属性を組み合わせることもまた、混乱の原因となります。

factory :post do
  published_at Time.now
end

first_post = build(:post)

Timecop.travel(2.days.from_now)

second_post = build(:post)

こんなコードを見ると、みなさんは別々の日時が2つのpostオブジェクトのpublished_atにセットされると思うかもしれません。しかし、作成されたpostオブジェクトには、どちらも同一の日時がセットされます。これはファクトリの定義が読み込まれた際の日時です。

永続化されたレコード(persisted records)を静的な属性としてセットする場合も、やはり混乱の原因となります。

factory :robot do
  friend Person.create!(name: "Daniel")
end

このファクトリを使うと、どのrobotオブジェクトも同一のDaniel氏と友達になります。Daniel氏にとっては嬉しいかもしれませんが、みなさんのテストスイートにおいては無意味かもしれません(訳注: ちなみに元記事の執筆者がDanielさんです)。

このDaniel氏の例はfactory_bot_railsを使ったときに、より悲惨なことになります。factory_bot_railsは通常、Gemfileのtestグループとdevelopmentグループの中に書かれます(このgemがdevelopmentグループで必要になる理由は、ジェネレータを使ったり、rails console内でレコードを作ったりするときに使われるからです)。factory_bot_railsはアプリケーションが読み込まれたタイミングでファクトリの定義を自動的に読み込みます。そのため、上のようなファクトリ定義があると、サーバを起動したり、コンソールを開いたりするたびに、毎回別のDaniel氏がデータベースに追加されていきます。私はどのDaniel氏も大好きです。ですが、物事には限度というものがあります。

こういった混乱を引き起こす可能性があるため、我々にとって静的な属性を残しておく意味はありません。factory_bot 4.11では静的な属性を非推奨とします。そしてfactory_bot 5で完全に削除します。

見た目的な問題で、ブロックなしで属性を定義したいという意見はたしかにありました。ですが、私にとってはブロックが必要かどうかをこれ以上考えずに済む方がずっとマシです。

非推奨の警告を見るのは楽しいことではありません。factory_botのユーザーが楽に移行できるよう、我々はrubocop-rspec Copに便利ツールを追加しました。rubocop-rspecをGemfileに追加し、以下のコマンドを実行すれば、簡単にアップグレードが完了するはずです。

rubocop \
  --require rubocop-rspec \
  --only FactoryBot/AttributeDefinedStatically \
  --auto-correct

RuboCopで頑張った点については、また別の機会に書くかもしれません。それまでの間に、みなさんが簡単にアップグレードできることを願っています!

(翻訳ここまで)

あわせて読みたい

Factory Botは以前、Factory Girlという名前で使われていたgemです。
名前が変更された経緯については以下の記事をご覧ください。

【翻訳】"Factory Girl"が"Factory Bot"に変わった理由(と移行手順) - Qiita