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. Fixturesの作り方 • YAMLで記述 ◦ 1ファイル1モデル • ラベルを使うことでレコードを識 別できる ◦ 右の例だと

    david と steve • 記載した内容がそのままデータ ベースに挿入される ◦ ActiveRecordインスタンスの 生成を介さない ◦ よってvalidationも行われない https://guides.rubyonrails.org/testing.html#the-low-down-on-fixtures
  2. Fixturesの参照方法 • 対応するテーブルごとにメソッドが用意される ◦ rspecの場合は上記のようにどの fixtureを使うかを明示的に指定 ◦ `config.global_fixtures` で spec

    全体で使えるようにすることも可能 • 上記の例だと、User / Organization モデルのfixturesで挿入されたデータを `users` / `organizations` メソッドで参照可能にしている。 ◦ `users(:LABEL_NAME)` で対応するレコードの ActiveRecordインスタンスを取得できる。
  3. 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
  4. 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)
  5. しかし実際は... • Fixturesへの批判はFactoryBotを使ったとしても結構当てはまってしまう • 「テストとテスト対象データが離れている」 ◦ 自分ひとりでfactoryを書いているうちは問題ないが、複数人でテスト書いていると factoryの定義すべてを確認 するわけでもなく、ブラックボックス化しやすい •

    「テストデータの管理が大変」 ◦ FixturesとFactory、それぞれで辛い点がある。 ◦ associationがどんどん増えていくとどちらにしても管理は辛い • 「テストしたい項目に対して不必要なデータが読み込まれる」 ◦ これはこれで正しい。 ◦ が、本番環境に近い状況を再現したいテストの場合、テストに必要なデータ「だけ」があるよりも多様なデータが あるほうが問題に気づきやすい、という見方も可能。
  6. Fixtureを使うメリット • テストデータの生成が速い ◦ DBに直接データを流し込むだけなので、 FactoryBotと異なりARインスタンス生成コスト がなくなる ◦ 反面、validationは行われず、各種callbackも当然実行されない •

    `use_transactional_test` との相性が良い。 ◦ テスト実行時に一度だけデータを流し込み、後のテストはすべてトランザクションで処理 し、ロールバックすることでクリーンナップ処理が高速化される。 ◦ Rails 5.1で導入されたSystemTestを使えばCapybaraを使ったテストでも `use_transactional_test = true` で問題ない。rspecで既存のfeature specを system spec に切り替えることもそれほど手間なくできる。
  7. 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
  8. レギュレーション • 完全な置き換えは難しいと思われるので、やれるところだけやる。 ◦ 最初は「全部置き換えてやるぜ!」と思っていたら無理ゲーでした • 置き換えるのは model specのみ ◦

    system specではCapybaraのオーバーヘッドが大きくてテスト時間が減少しても評価しづ らいと想定 • upstreamにPRしない想定 ◦ あくまで実験 ◦ fixtureのラベル名がかなり雑だったり、謎にこける shoulda-matcherなどはコメントアウト したり • 単純にFactoryからFixtureに変換すれば、変えた分だけ速くなる、はず。 ◦ dev.toは Rails 5.2 で `use_transactional_test = true` になっている
  9. dev.toのざっくりなモデル構成解説 • User / Organiztion ◦ GitHubのように1ユーザが複数組織に所属可能 ◦ Organizationは組織用のページを持てたりする。詳細は https://dev.to/organization-info

    を参照 • Article ◦ 記事に対応するモデル ◦ ここにコメントや投票とかがいろいろ紐づく • モデルやテーブルの数はそれほど多くない(個人的印象)
  10. 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 に指定すると無駄なインスタンス生成が起こる(はず)。
  11. 置き換えてみた • なぜか何もしていないのにこけるテストが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が今回の置き換えで影響を受けたテスト。
  12. 計測方法 • forkした時点のmasterとfixture対応したブランチでの “rspec spec/models” 実行時間比較 ◦ 実行時間はrspecが最後に出力する “Finished in

    ….” の値を採用 • 手元の開発環境でなるべく余分な負荷をかけずに10回ずつ実行 ◦ 開発環境は Ubuntu 18.04 on Hyper-V (Windows 10) ◦ 厳密にやるならLinuxが動いている実機でやるべきですが、自宅に都合の良いマシンが 落ちてなかった...
  13. 辛かったところ • 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を利用。
  14. Fixture一番の弱点:ラベル(名前)付けが必要 • fixtureでは常にテストデータがすべて DB上に存在する ◦ よって、識別するための名前付けが必要。 • 大量の組み合わせデータを作るのが辛い ◦ 「複数Userが別々のOrganizationに所属してそれぞれ記事を書いている」というテストデータと「複数

    Userが 別々のOrganizationに所属してそれぞれ書いた記事にコメントつける」というテストデータが常時 DBにあって、 それぞれをspec側から区別できるネーミングをつけるというのがとにかく辛い。 ◦ 適切に名前付けして識別できればデータベース上にテストに必要な全データが入っていてもそれほど大変では ない(ような気がする) • Factoryは無名関数的に使えるのが強いのだと実感 ◦ テスト実行中にしかデータが存在しないので、コンテキスト内で変数名で識別できれば十分 ◦ trait等を利用するまでは名前付けを遅延できる
  15. 気づいたこと • 改めて見直すと「別に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があればいいのかも。
  16. テスト実行時間削減としてのFixture利用 • 既存のテストへの導入はコスパが悪いかもしれない ◦ 短期的に作業時間にかかるエンジニアの人件費のほうがCIでインスタンス追加するより高くつく ◦ 超長期で見たらペイするかもしれないが... • 新規プロジェクトで導入するのは合理的 ◦

    長期的にCIのコストを抑制する方向に働く • 実行時間削減のためにFixtureのみの使用に執着するのは本末転倒 ◦ 「それほど手間は違わないから、ここはFactory使わずFixtureにしておきましょう」くらいが良いか 13秒改善のために43コミット・ 77ファイルの変更 (自動生成yamlファイル等含む)
  17. まとめ • Rails Fixturesについて振り返り、実際に運用されているサービスである dev.to のコードでやれる範 囲でFactoryBotによるテストデータ生成を Rails Fixturesで置き換えてみました。 •

    テスト実行時間については有意な差が出た(と思う)のですが、積極的に FactoryBotから書き換え るべきかというと... • Factory / Fixture双方の使い方について再度考える良い機会だった。 ◦ 複数のモデルで関連や attributeが異なるデータを多数作るなら Factoryが有利 ◦ AR callbacksの挙動を試す場合、 Factoryを使うほうが楽。 ◦ (テストであっても)件数が多いマスターデータなどの場合は fixtureを使うと良い。 ◦ そもそも永続化しないでテストできる部分は FacotryもFixtureも使わないほうが良い。
  18. 感想 「fixtureで高速化」という点で解決できる問題と、そうでない問題がありそう。 - AR callbacksを使っていて、それがモデルの状態に影響するクラスは厳しい - FactoryBotのcallbackやtransientを使っている場合、置き換えはつらい - specごとに読み込むfixtureを分ける、というアプローチは悪くないかも -

    テスト開始時に一度だけ読み込む、というアプローチではないが、それでも AR経由でのレコード保存を減らせる - 予めFactoryBotで記述したデータをfixtureにdumpしてそれをfixtureとして利用できないか? - 上述のspecごとに読み込むfixtureを分けるというアプローチと組み合わせて - テスト実行前に行う、テストデータのキャッシュ生成みたいなもの。
  19. 得た学び - 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` にマッピングしてくれるとか