Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rails Fixtures再考

Rails Fixtures再考

Masatoshi Iwasaki

September 12, 2019
Tweet

More Decks by Masatoshi Iwasaki

Other Decks in Programming

Transcript

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

    View full-size slide

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

    View full-size slide

  3. 今日のテーマ
    Rails Fixtures 再考

    View full-size slide

  4. そもそも
    再考
    する前に

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  9. 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

    View full-size slide

  10. 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)

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  18. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  22. 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 に指定すると無駄なインスタンス生成が起こる(はず)。

    View full-size slide

  23. 置き換えてみた
    ● なぜか何もしていないのにこけるテストが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が今回の置き換えで影響を受けたテスト。

    View full-size slide

  24. 実効速度の比較

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. Factory -> Fixture してみた感想

    View full-size slide

  29. 辛かったところ
    ● 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を利用。

    View full-size slide

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

    View full-size slide

  31. 気づいたこと
    ● 改めて見直すと「別に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があればいいのかも。

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  36. 得た学び
    - 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` にマッピングしてくれるとか

    View full-size slide