はじめに

不安定でテストがほとんどされていないアプリケーションを、効果的なテストの実践によって堅牢な製品へと変える方法を紹介します。

スタートアップの若いプロジェクトに参加すると、しばしばいくつかのベストプラクティスが欠けている状況に直面します。そして、どこから手をつけるべきか判断するのは簡単ではありません。

この記事では、簡単に状況を説明したあとに、生産性を損なうことなくテストスイートをすばやく改善するための、4つのシンプルなステップによる効果的なアプローチを紹介します:安定化、ベストプラクティスの導入、既存テストの整理、そして最適化です。

最初は:テストが少なくバグが多い…

Webアプリケーションの歴史は常に複雑です。コードベースは開発者とともに進化し、プロジェクトごとの歴史に応じたさまざまなフェーズを経ていきます。経験豊富なエンジニアの大規模なチームと潤沢な予算で始まる場合と、新卒の小さな開発チームで始まる場合とでは、課題が異なります。

ここで取り上げるプロジェクトは、中堅の経験を持つ開発者たちによって、野心的な計画と限られた予算の中で始まりました。そのため、テストやベストプラクティスにも配慮しつつ、迅速な開発を優先するという戦略が採られました。

しかしアプリケーションは十分にテストされておらず、それが安定性に影響していました。

フェーズ1:安定性向上のために素早く行動

コードの重要な部分が十分にカバーされていませんでした。この問題を解決するために、Railsのシステムテストを使用しました。このテストの基本原則は、ブラウザを自動で操作することです。つまり、テストコードがブラウザにどのページに移動し、どのアクション(リンクをクリックする、フォームを入力するなど)を実行するかを指示し、結果を確認します。他のテストタイプと比べて遅いですが、その分サイトのインタラクティブ性を検証できます。

私たちの場合、複数のフォームを連続して入力することで、1つのユースケース全体をテストするケースがありました。このアプローチの利点は、プロジェクトの広範囲をカバーするテストをすばやく追加できることです。

状況に対してこのアプローチは適していましたが、欠点もありました:

これらのテストは経年劣化します。アプリケーションが進化・複雑化するにつれて、テストも複雑で壊れやすくなります。たとえば、住所の自動入力を可能にするコンポーネントを追加しましたが、多くのフォームで住所を入力していたため、このコンポーネントの繰り返し利用によりテストが長くなり、ランダムに失敗しやすくなりました。最終的に、そのテストは頻繁に修正と調整を要するようになりました。

そして、このようなテストはもともと問題のある状況を是正するために導入されたものですが、一部の開発者が新しい機能開発の際にも参考例として使ってしまいました。

このアプローチは最初に問題解決のためには有効ですが、コードの将来的な変化を見越すことが重要です。

フェーズ2:持続可能なベストプラクティスの導入

しばらくの間は、状況は悪くありません。たとえテストが遅く、一部のコードが十分にテストされていなくても、アプリケーションの安定性は受け入れられるレベルであり、チームの生産性も良好でした。

しかし、時間が経つにつれて、テストの失敗率が増加し始め、それに伴いテスト時間も大幅に増え、生産性に問題を引き起こすようになります。

そこで私たちは、より厳密にテストに取り組むことを決め、以下の原則を適用しました:

  • テストカバレッジを増やしたいが、極端には走らない。開発者には注意深く、より多くのテストを書くよう推奨しました。これにより、既存コードの修正にも自信を持てるようになります。最初は少し過剰に思えるかもしれませんが、プロジェクトが複雑になるにつれて、良好な機能カバレッジはリグレッションを防ぐために不可欠です。
  • システムテストよりも他の種類のテストを優先するように求めました。これにより、パフォーマンスが向上します。たとえば、フォームを送信して結果を見るテストは、コントローラテストで代替できます。実行時には、システムテストよりもはるかに高速です。
  • テスト内でのsleepの使用は厳禁とし、すべての既存テストからsleepを削除する、または該当テスト自体を削除する計画を立てました。
  • JavaScriptを使うコンポーネントに対してのみ、こうしたシステムテストの使用を許可しました。同時に、アプリケーション全体でのJavaScript使用を極力控えました。

これらのルールを導入することで、すぐに安定性が向上しました。ただし、既存のテストに関する問題はこれだけでは解決できません。将来的には、テストの量や実行時間が急激に増加しないように管理していく必要があります。

フェーズ3:既存テストの整理

ベストプラクティスが導入されたら、次は過去の遺産に取り組む時です。

コストが高く壊れやすいテストを残すより、より焦点を絞ったユニットテストや統合テストに置き換える方が効果的な場合が多いです。

ベストプラクティスのおかげで、不足を補うために導入されたシステムテストの大部分は、より的確で安定したテストによってカバーされるようになりました。残りは、通常変更の必要がない非常に安定した機能をカバーしています。どちらの場合も、元のテストは単純に削除して構いません。

整理の仕上げとして、最も時間がかかるテストと重複しているテストを特定しました。

時間がかかるテストを見つけるには、次のコマンドを使用します:

rails test -v | sort -t = -k 2 -g

このコマンドは、Railsのテストを詳細モードで実行し、実行時間順に並べて長いものから表示します。

重複しているテストを見つけるにはコマンドはありませんが、開発中に徐々に気づくことができます。ある変更によって複数のテストが失敗する場合、不要な重複の可能性があります。

結果として、いくつかのシンプルな原則を適用するだけで、テストスイートの実行時間を約30%短縮できました。実行時間は約20分から15分未満になりました。

フェーズ4:パフォーマンス向上のための実用的アプローチ

さらに時間をかけて最適化することもできましたが、それにはすべてのテストの完全かつ詳細な監査が必要で、長期的な作業になります。テストを削除またはすばやく修正できる場合は良いですが、そうでない場合はより多くの労力が必要です。

このような場合には、前述のベストプラクティスを適用するにとどめ、他には以下のような実用的な戦略を取り入れてパフォーマンスを改善しました:

  • より高速で柔軟、かつ特に高価でもない別のサービスプロバイダに切り替えました。
  • 同時に、複数のマシンでタスクを並列実行するように構成を変更しました。

その結果、15分弱だった実行時間が6分になりました。

これは良い結果ですが、テスト作成時のベストプラクティスが伴わなければ、テスト数が増加し、それに伴ってテスト実行マシンの数も増え、コストが上昇してしまいます。

結論:適切に保守されたテストスイート

この段階的なアプローチにより、問題の多い出発点にもかかわらず、非常に満足のいく状況に到達できました。最も緊急性の高い問題に着手し、次に持続可能なルールを定め、遺産の問題を修正し、最後に的を絞った整理を行うことで、過度に体系的なアプローチに比べて無駄な時間を費やすことなく、効果的な成果を上げることができました。

この実用的かつ反復的な方法は、大規模なリファクタリングプロジェクトに特に適しています。全体的なアプローチで素早く大きな部分を処理したうえで、細部を洗練させていくという進め方です。これはソフトウェア開発の他の領域にも適用可能な、シンプルかつ効果的な方法です。少し手がつけられていなかったコードベースを再び掌握する際に役立ちます。

結局のところ、鍵となるのは同じです:段階を踏んで進め、最初に素早い成果を出し、長期的に継続的な改善を行うこと。この方法なら、複雑なコードベースを少しずつ掌握しながら、チームの安定性と生産性を維持することができます。