Railsのテスティングピラミッド(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Railsのテスティングピラミッド(翻訳)

テストの速度が期待より遅くなっていませんか?それとも、テストが不安定でリファクタリングやアプリ機能の根本的な変更もままならない状況になっていませんか?長年運用されている大規模なRailsアプリではこうした不満をよく目にしますが、テスティングのためのよい手法が浸透していれば、こうした問題が長期に渡って発生することはないはずです。

「テスティングピラミッド」のコンセプトは、テストのバランスの維持やテストスイートの高速化、アプリの機能変更コストの削減に有用であり、テストスイートにおけるさまざまな種類のテストを組み立てるときの中心に位置づけられます。

Rails Testing Pyramid

高速な単体テストの数は多く、低速な結合テストの数は少なくする

Railsで最もよく知られている2種類のテストについては皆さんも既に実地でお馴染みかと思います。

  • 単体テスト(unit test): 最も下位にあり、かつ最も重要なレベルです。単体テストではRSpecやMiniTest、Jasmineといったツールを用いて、機能(クラス、メソッド、関数が典型的)の個別の単位の振る舞いが正しいことを確認し、極めて高速(msec単位)に実行されます。
  • 受け入れテスト(acceptance test): RSpecとCapybara/Cucumber/Seleniumなどのツールを用いる上位レベル(ユーザー操作レベルが典型的)のテストです。コード量は単体テストよりもずっと多く、外部サービスに依存することも多いため速度は遅くなります(秒単位や分単位)。

アプリ機能を正しくテストするには、単体テストと受け入れテストの両方が必要です。最初のうちは単体テストのカバレッジ値を上げることに注力しましょう。これはテスト駆動開発(TDD)ワークフローの自然な副産物です。単体テストではエッジケースを捉え、オブジェクトの正しい振る舞いを確認すべきです。

単体テストのカバレッジ値を上げたら、次はエンドユーザーのようにアプリを操作する受け入れテストを用いて、単体テストを丁寧に補完しましょう。これによってすべてのオブジェクトが協調動作していることを確信できます。チームでありがちなのは、受け入れテストをたくさん作りすぎてしまうことで、これをやってしまうと開発サイクルの速度が落ちてしまいます。ユーザー登録をテストするのにCapybaraベースのテストを20個も使ってすべてのバリデーションエラーが正しく扱われていることを確認しているなら、そのテスティングのレベルは正しくありません。これは逆テスティングピラミッド(inverted testing pyramid)と呼ばれるアンチパターンです。

使い捨てテスト

使い捨てテストの有用性に気づくことは重要です。取っておく必要のない使い捨てテストは、ちょうどscaffoldに似ています。2週間かけて複雑な登録システムを作ることになったとしましょう。このとき、一日のうち半分をいくつものCucumberテストの動作検証からなるさまざまな「ストーリー」の作成に当てるのはよい開発手法です。細かく分割された「ストーリー」を多数用意して、それらが受け入れテストをパスするという形で各ストーリーの完了を明確に確認できるからです。この場合、最終的にテストスイートを削減し、動作に確信を持てる最小限までテストセットを絞り込むことをお忘れなく(どこまで絞り込むかは機能によります)。

市場に出せる最小限の機能が備わったら、ストーリー作成で使った40〜50個もの受け入れテストを残したままリリースするのではなく、その登録システムでの主要なフロー(ユーザーの動線)をカバーする3〜5個程度の受け入れテストに置き換えるべきです。その他のテストは捨てても構いませんが、主要な振る舞いの正しさは担保してください(多くの場合単体テストの追加によって行います)。これを守らないと、やがてテストスイートを5〜8分で完了するためだけに本格的な並列実行の導入が必要になってしまい、UIをわずかに変更しただけで大量のテストが失敗するようになってしまいます。

サービスレベルテスト

ときどき、単体テストのレベルに該当するとは考えにくく、かといってUI経由でテストすべきでもない機能が最終的に見つかることがあります。『Succeeding with Agile』(Mike Cohn, 2009年)という書籍で使われた「テスティングピラミッド」というコンセプトは後にMartin Fowlerによって有名になりましたが、同書ではこうしたテストを「サービスレベルテスト」というフレーズで説明しました。この用語は多くのコミュニティで、単体テストとエンドツーエンドの受け入れテストの間を埋める機能テストや結合テストを指すのに使われています。

サービスレベルテストで重要なのは、アプリやサブシステムのAPIを公開して、「APIを実行するUIとは独立に」APIをテストできるようにする点です。Webやネイティブモバイルクライアントにサーバーサイドでサービスを提供する単一のRESTful JSON APIの構築を目指すことが多くの開発チームでトレンドになっていますが、サービスレベルテストのコンセプトはこうしたWeb開発の潮流とうまく結びついています。

ひとつにまとめる

多くのアプリでは、ユーザーにとって重要な動線はごくわずかにとどまるものです。eコマースアプリの場合なら次のあたりを押さえておくことになるでしょう。

  1. 製品カタログの閲覧
  2. 製品の購入
  3. アカウントの作成
  4. ログイン(パスワードリセットも含む)
  5. 注文履歴の確認

この5つが正常に機能する限り、開発者が夜中に叩き起こされてアプリのコードを修正する必要は生じません(運用はまた別の話ですが)。こうした機能は、2分もかからずに実行できる5つのおおまかなCapybaraテストでカバーできるでしょう。

単体テスト/サービスレベルテスト/受け入れテストを「適切な割合で」整備することで、アプリの動作を担保できるテストスイートをより短期間に作成でき、突然動かなくなるような事態への耐性も高まります。作業が進んだら、貢献度の低いテストを削るようにしましょう。バグ修正時には、問題を再現するテストを可能な限り「低レベルで」実装しましょう。常日頃から、受け入れテストの実行時間に注意するのと同様、テストの種類ごとの個数がどのような割合になっているかにも注意を向けるようにしましょう。

この手法を用いることで、テストの理想的な境地を達成できます。テストスイートによって正常な動作を確信できるようになれば、機能を損なったりUI関連のテストが失敗したりせずに機能を自由に追加でき、並列実行なしでも数分以内にテストを完了できるようになります。

Peter BellはSpeak Geekの創立者兼CTOであり、GitHubトレーニングチームの契約メンバーであり、JavaScriptやRubyでの開発からDevOpsやNoSQLデータストア関連のあらゆるトレーニングやコンサルティングを手がけています。

関連記事

Rails: テストのリファクタリングでアプリ設計を改良する(翻訳)

Rubyのクラスメソッドがリファクタリングに抵抗する理由(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

夏のTechRachoフェア2019

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ