Slide 1

Slide 1 text

Rails Fixtures 再考 2019/09/12 銀座Rails #13 Masatoshi Iwasaki

Slide 2

Slide 2 text

自己紹介 Masatoshi Iwasaki GitHub: masa-iwasaki Twitter: masa_iwasaki 今月からフリーランスのRailsエンジニアになりました (3.5年振り2回目)

Slide 3

Slide 3 text

今日のテーマ Rails Fixtures 再考

Slide 4

Slide 4 text

そもそも 再考 する前に

Slide 5

Slide 5 text

会場アンケート - Fixtures知っている人? - Fixtures使っているプロジェクトに関わったことがある 人? - Fixtures実際に書いたことがある人? - テストデータFixturesだけで書いたRailsプロジェクトが ある人?

Slide 6

Slide 6 text

Fixturesとは Railsに標準で用意されているテスト用サンプル データを用意する仕組み。 Rails GuidesのTestingに関するセクション(右記) に説明がある。 https://guides.rubyonrails.org/testing.html#the-low-down-on-fixtures

Slide 7

Slide 7 text

Fixturesの作り方 ● YAMLで記述 ○ 1ファイル1モデル ● ラベルを使うことでレコードを識 別できる ○ 右の例だと david と steve ● 記載した内容がそのままデータ ベースに挿入される ○ ActiveRecordインスタンスの 生成を介さない ○ よってvalidationも行われない https://guides.rubyonrails.org/testing.html#the-low-down-on-fixtures

Slide 8

Slide 8 text

Fixturesの参照方法 ● 対応するテーブルごとにメソッドが用意される ○ rspecの場合は上記のようにどの fixtureを使うかを明示的に指定 ○ `config.global_fixtures` で spec 全体で使えるようにすることも可能 ● 上記の例だと、User / Organization モデルのfixturesで挿入されたデータを `users` / `organizations` メソッドで参照可能にしている。 ○ `users(:LABEL_NAME)` で対応するレコードの ActiveRecordインスタンスを取得できる。

Slide 9

Slide 9 text

Fixtures YAMLでできること ● erbを使って動的に値を生成したり、ラベルを使ってassociationを簡単に記載できる。 ○ 時間の都合で割愛。 ● ActiveRecord::FixtureSet のドキュメントに詳細がまとまっています。 ○ https://api.rubyonrails.org/v6.0.0/classes/ActiveRecord/FixtureSet.html ○ 日本語訳もあります。 https://techracho.bpsinc.jp/hachi8833/2019_07_11/77512 ● RubyKaigi 2014の Deep down fixtures のスライドも手短に内容がまとまっている ○ http://rubykaigi.org/2014/presentation/S-PrathameshSonpatki/ ○ ...と思ったらいつの間にかスライドが404だった ● 登壇者が追加として記載した以下のTipsも役立つ ○ https://blog.bigbinary.com/2014/09/21/tricks-and-tips-for-using-fixtures-in-rails.html

Slide 10

Slide 10 text

FactoryBot vs Rails Fixtures ● (たぶん)多くの開発現場ではテスト用サンプルデータ作成に FactoryBotのほうが多用されている ● Fixturesに対する否定的・批判的な意見が多く、中には “Fixturesは使うな” という意見もある。 ○ Rails Testing Antipatterns: Fixtures and Factories (https://semaphoreci.com/blog/2014/01/14/rails-testing-antipatterns-fixtures-and-factories.html) ● 否定体な意見のポイントは概ね以下のように集約できる(と思う) ○ 「テストとテスト対象データが離れている」 ○ 「テストデータの管理が大変」 ○ 「テストしたい項目に対して不必要なデータが読み込まれる」 ● FactoryBotを開発しているthoughtbotが書いている以下の記事がシンプルかつ要点を抑えている ○ Mystery Guest (https://thoughtbot.com/blog/mystery-guest)

Slide 11

Slide 11 text

しかし実際は... ● Fixturesへの批判はFactoryBotを使ったとしても結構当てはまってしまう ● 「テストとテスト対象データが離れている」 ○ 自分ひとりでfactoryを書いているうちは問題ないが、複数人でテスト書いていると factoryの定義すべてを確認 するわけでもなく、ブラックボックス化しやすい ● 「テストデータの管理が大変」 ○ FixturesとFactory、それぞれで辛い点がある。 ○ associationがどんどん増えていくとどちらにしても管理は辛い ● 「テストしたい項目に対して不必要なデータが読み込まれる」 ○ これはこれで正しい。 ○ が、本番環境に近い状況を再現したいテストの場合、テストに必要なデータ「だけ」があるよりも多様なデータが あるほうが問題に気づきやすい、という見方も可能。

Slide 12

Slide 12 text

ということで、今日のテーマ Rails Fixtures 再考

Slide 13

Slide 13 text

Fixtureを使うメリット ● テストデータの生成が速い ○ DBに直接データを流し込むだけなので、 FactoryBotと異なりARインスタンス生成コスト がなくなる ○ 反面、validationは行われず、各種callbackも当然実行されない ● `use_transactional_test` との相性が良い。 ○ テスト実行時に一度だけデータを流し込み、後のテストはすべてトランザクションで処理 し、ロールバックすることでクリーンナップ処理が高速化される。 ○ Rails 5.1で導入されたSystemTestを使えばCapybaraを使ったテストでも `use_transactional_test = true` で問題ない。rspecで既存のfeature specを system spec に切り替えることもそれほど手間なくできる。

Slide 14

Slide 14 text

湧き上がる素朴な疑問 FactoryBotをFixturesに 置き換えたらテスト早く なるのでは?

Slide 15

Slide 15 text

どうやって検証するか? ● 簡単なプロジェクトだと実際のプロジェクトと比較しづらい ○ 実際に動いているRailsプロジェクトが良い ● あまりにでかいプロジェクトだと置き換えるのがつらい。 ● かつ、OSSだと具体的な話を公開・共有できるので理想。

Slide 16

Slide 16 text

そんな条件を満たすプロジェクトあるの?

Slide 17

Slide 17 text

ありました https://github.com/thepracticaldev/dev.to

Slide 18

Slide 18 text

dev.toとは? - ソフトウェアエンジニアが技術情報を投稿するサイト ( - https://dev.to - ざっくりいうとQiitaみたいなもの - 一時期「表示が爆速」ということで話題になった - 2018/08からサービスのソースコードがオープンソースになっている - https://dev.to/ben/devto-is-now-open-source-5n1 ちょうど一年前の ginza.rbでテーマに取り上げられていまし た。 https://ginzarb.doorkeeper.jp/events/79974

Slide 19

Slide 19 text

dev.toでFactoryをFixtureに置き換えられるか? ● dev.toで実験的に置き換えを行えば、それなりに現実的な知見が得られる。 ● (もし)それなりの高速化ができれば fixtureのメリットについて評価が可能。 ○ 置き換えのコストと高速化により減少したテスト実行時間とのトレードオフ

Slide 20

Slide 20 text

レギュレーション ● 完全な置き換えは難しいと思われるので、やれるところだけやる。 ○ 最初は「全部置き換えてやるぜ!」と思っていたら無理ゲーでした ● 置き換えるのは model specのみ ○ system specではCapybaraのオーバーヘッドが大きくてテスト時間が減少しても評価しづ らいと想定 ● upstreamにPRしない想定 ○ あくまで実験 ○ fixtureのラベル名がかなり雑だったり、謎にこける shoulda-matcherなどはコメントアウト したり ● 単純にFactoryからFixtureに変換すれば、変えた分だけ速くなる、はず。 ○ dev.toは Rails 5.2 で `use_transactional_test = true` になっている

Slide 21

Slide 21 text

dev.toのざっくりなモデル構成解説 ● User / Organiztion ○ GitHubのように1ユーザが複数組織に所属可能 ○ Organizationは組織用のページを持てたりする。詳細は https://dev.to/organization-info を参照 ● Article ○ 記事に対応するモデル ○ ここにコメントや投票とかがいろいろ紐づく ● モデルやテーブルの数はそれほど多くない(個人的印象)

Slide 22

Slide 22 text

Factory -> Fixture置き換えの手順 ● GitHub上でfork ○ masterの最終コミット(0133cf1)が2019/07/26時点のもの ● Facotry定義ファイルをざっくりとFixturesのyamlに置き換えるgemを作成 ○ https://github.com/masa-iwasaki/factory_scrap ○ マジでざっくりなので使うときはほんと注意してください ○ transientとかファイルアップロードとかは自動的に直せないため、手動で修正が必要。 ● app/models/*.rb で create(: で始まる箇所を検索。 ● 手作業で fixtureに置き換える。 ● 必要な箇所だけ `fixtures :users` のように fixtureを参照して、global_fixturesや `:all` は使わない。 ○ `fixtures :users` が実行された段階で users テーブルに存在するレコードがすべてインスタンス化 されるため、rspecの global_fixtures に指定すると無駄なインスタンス生成が起こる(はず)。

Slide 23

Slide 23 text

置き換えてみた ● なぜか何もしていないのにこけるテストが2件ほどあったので、そこについては飛ばした ○ どれもshoulda-matcher関連なので実行速度への影響は軽微。 ● CIで安定的にテストが通るところまでもってこれた ○ https://github.com/masa-iwasaki/dev.to/pull/1 ○ Forkしたmasterでfixtureとは無関係にコケるテストを修正した比較用PRも用意 https://github.com/masa-iwasaki/dev.to/pull/2 ● 手作業で修正・追加したfixtureについては実装速度優先でだいぶ雑に記載 ○ コピペや雑なラベル名(user_1, user_2)など。 ● model spec 52ファイル中、25ファイルで一部 or 全部fixture置き換え ○ 全体で641 examplesのうち、247 examplesが今回の置き換えで影響を受けたテスト。

Slide 24

Slide 24 text

実効速度の比較

Slide 25

Slide 25 text

計測方法 ● forkした時点のmasterとfixture対応したブランチでの “rspec spec/models” 実行時間比較 ○ 実行時間はrspecが最後に出力する “Finished in ….” の値を採用 ● 手元の開発環境でなるべく余分な負荷をかけずに10回ずつ実行 ○ 開発環境は Ubuntu 18.04 on Hyper-V (Windows 10) ○ 厳密にやるならLinuxが動いている実機でやるべきですが、自宅に都合の良いマシンが 落ちてなかった...

Slide 26

Slide 26 text

実行速度比較結果 約13秒(平均)の実行時間減少。

Slide 27

Slide 27 text

TravisCI上での比較 1度しか走らせていないものの約 15秒の改善と開発環境での実行時間差に近い。 master: 181.4 seconds / fixture置き換え: 166.5 seconds 181.4 - 166.5 = 14.9 ↑ master / ↓ fixture置き換え

Slide 28

Slide 28 text

Factory -> Fixture してみた感想

Slide 29

Slide 29 text

辛かったところ ● associationが無いモデルは楽勝(当たり前) ● callbackの実行をテストしているところは引き続きFactoryを使う ○ before_create / before_validationとか ○ 特にArticleはcallbackが多く、associationでArticleを参照しているモデルのmodel specでarticleの callbackによる変更を確認したりするテストをfixture化するのはつらい。 ● Factoryで transient や 各種callbackを使っているところFactory継続 ○ Factory不要の状態に持っていくことは可能だが、Fixtureに直すという趣旨からは外れる ● ファイル添付可能なモデルもFactory継続 ○ dev.toはCarrierWaveを利用。

Slide 30

Slide 30 text

Fixture一番の弱点:ラベル(名前)付けが必要 ● fixtureでは常にテストデータがすべて DB上に存在する ○ よって、識別するための名前付けが必要。 ● 大量の組み合わせデータを作るのが辛い ○ 「複数Userが別々のOrganizationに所属してそれぞれ記事を書いている」というテストデータと「複数 Userが 別々のOrganizationに所属してそれぞれ書いた記事にコメントつける」というテストデータが常時 DBにあって、 それぞれをspec側から区別できるネーミングをつけるというのがとにかく辛い。 ○ 適切に名前付けして識別できればデータベース上にテストに必要な全データが入っていてもそれほど大変では ない(ような気がする) ● Factoryは無名関数的に使えるのが強いのだと実感 ○ テスト実行中にしかデータが存在しないので、コンテキスト内で変数名で識別できれば十分 ○ trait等を利用するまでは名前付けを遅延できる

Slide 31

Slide 31 text

気づいたこと ● 改めて見直すと「別にFactory使わなくていいよね?」というテストも見つかる。 ○ thoughtbotのブログでも「ほとんどのユニットテストは永続化されたデータ(=DBへの保存)は要らない からfactoryを使わないほうがテストを高速化できるよ」という記事を書いている。 ■ Speed Up Tests by Selectively Avoiding Factory Girl (https://thoughtbot.com/blog/speed-up-tests-by-selectively-avoiding-factory-girl) ○ FactoryBot自体が ひたすら instance_eval でファクトリーの定義を評価していて、ARインスタンス生成コ スト以上のコストが掛かっていることも考慮したい。 ● ARのcallbackを多用しているモデルは置き換えづらい。 ○ 反対にマスターデータなどはFixturesで予め流し込んでおくほうが良い。 ● Fixturesで流したデータのvalidationは必須 ○ expect(user).should be_valid というようなテストでfixtureの不備に気付かされたことが多数。 ○ テストとは別にfixturesのデータがvalidかどうかをチェックするrake taskがあればいいのかも。

Slide 32

Slide 32 text

テスト実行時間削減としてのFixture利用 ● 既存のテストへの導入はコスパが悪いかもしれない ○ 短期的に作業時間にかかるエンジニアの人件費のほうがCIでインスタンス追加するより高くつく ○ 超長期で見たらペイするかもしれないが... ● 新規プロジェクトで導入するのは合理的 ○ 長期的にCIのコストを抑制する方向に働く ● 実行時間削減のためにFixtureのみの使用に執着するのは本末転倒 ○ 「それほど手間は違わないから、ここはFactory使わずFixtureにしておきましょう」くらいが良いか 13秒改善のために43コミット・ 77ファイルの変更 (自動生成yamlファイル等含む)

Slide 33

Slide 33 text

まとめ ● Rails Fixturesについて振り返り、実際に運用されているサービスである dev.to のコードでやれる範 囲でFactoryBotによるテストデータ生成を Rails Fixturesで置き換えてみました。 ● テスト実行時間については有意な差が出た(と思う)のですが、積極的に FactoryBotから書き換え るべきかというと... ● Factory / Fixture双方の使い方について再度考える良い機会だった。 ○ 複数のモデルで関連や attributeが異なるデータを多数作るなら Factoryが有利 ○ AR callbacksの挙動を試す場合、 Factoryを使うほうが楽。 ○ (テストであっても)件数が多いマスターデータなどの場合は fixtureを使うと良い。 ○ そもそも永続化しないでテストできる部分は FacotryもFixtureも使わないほうが良い。

Slide 34

Slide 34 text

御清聴ありがとうございました ご意見・ご質問等あれば懇親会で!

Slide 35

Slide 35 text

感想 「fixtureで高速化」という点で解決できる問題と、そうでない問題がありそう。 - AR callbacksを使っていて、それがモデルの状態に影響するクラスは厳しい - FactoryBotのcallbackやtransientを使っている場合、置き換えはつらい - specごとに読み込むfixtureを分ける、というアプローチは悪くないかも - テスト開始時に一度だけ読み込む、というアプローチではないが、それでも AR経由でのレコード保存を減らせる - 予めFactoryBotで記述したデータをfixtureにdumpしてそれをfixtureとして利用できないか? - 上述のspecごとに読み込むfixtureを分けるというアプローチと組み合わせて - テスト実行前に行う、テストデータのキャッシュ生成みたいなもの。

Slide 36

Slide 36 text

得た学び - FactoryBotは定義の各行を instance_eval しているのでActiveRecordオブジェクトの生成コスト以上に 遅い。 - `fixtures` メソッドを呼び出した段階で、インスタンスが生成される - `fixtures :users` を呼び出すと、`users.yml` に入っているレコードすべてがインスタンス化される - レコードの多いfixtureを rspec-railsの `config.global_fixture` で使いたい場合は注意。 - `rspec-rails` がところどころ Rails Fixturesをwrapしていて、本体との違いが分かりづらい箇所もある。 - Rails 5.1以降でも `use_transactional_fixtures` を `use_transactional_test` にマッピングしてくれるとか