リグレッションとは、コードに変更を加えた後に現れるバグで、同じ機能が以前は正しく動作していたにもかかわらず発生します。リグレッションとは、新しい変更が以前に書かれてテストされたものを「壊した」ことを意味します。これは開発において最も一般的で危険な問題の1つです。1つのバグを修正する際に、開発者が気づかずに他の3つの機能を壊す可能性があります。Capers Jones Software Engineering 2023によると、リグレッションバグの平均密度は、変更されたコード100行あたり1~3個です。リグレッションの原因、検出方法、防止戦略を解説します。
重要なポイント
リグレッションとは、以前のバージョンで動作していた機能が、変更を加えた後に動作しなくなる状況です。変更は、バグ修正、新機能の追加、リファクタリング、ライブラリの更新、設定変更など、何でも該当します。リグレッションは安定性の最大の敵です。すべての変更は、すでに検証されてリリースされたものを壊すリスクを伴います。
この用語はテストから来ています。リグレッションテストとは、各変更後に既存のテストを再実行して、古い機能が壊れていないことを確認するプロセスです。以前は合格していたテストが失敗した場合、リグレッションが発生したことになります。広い意味では、リグレッションとはテストの失敗だけでなく、ユーザーやQAが気づく動作の低下も含みます。Tricentis State of Testing 2023によると、本番環境で見つかる全バグの35~45%がリグレッションです。
リグレッションを通常のバグと区別するのは時間的文脈です。通常のバグは最初から存在していた可能性がありますが、リグレッションは常に変更の結果です。この区別は重要です。なぜなら、リグレッションの原因を見つけるには、「動作していた」状態と「動作しなくなった」状態の間で何が変わったかを分析することから始まるからです。Git bisectは、リグレッションを引き起こしたコミットを見つけるための標準ツールです。
ローカルリグレッション — モジュールAの変更が同じモジュールAの機能を壊します。例:開発者がソート関数を書き換えたところ、空の配列を正しく処理できなくなる。ローカルリグレッションは原因と結果が近いため、最も検出と修正が容易です。
リモートリグレッション — モジュールAの変更が、コードで直接接続されていないがデータやタイミングで接続されているモジュールBの機能を壊します。例:「ユーザー」モジュールのデータベーススキーマ変更が、同じテーブルを使用する「アナリティクス」モジュールのレポートを壊す。リモートリグレッションは最も厄介です。開発者は自分の変更が他のモジュールに影響するとは予想しません。
副作用リグレッション — 副作用(ロギング、キャッシング、通知送信)の変更が期待される動作を壊します。例:開発者がパフォーマンス向上のためにキャッシュを追加したが、古いキャッシュのためにユーザーが古いデータを見る。副作用リグレッションは自動テストで検出するのが困難です。なぜなら副作用はしばしばテストでカバーされていないからです。
パフォーマンスリグレッション — コードは機能的に正しく動作し続けるが、以前より遅くなります。例:新しい暗号化アルゴリズムが同じ結果を生成するが、実行時間が2msから200msに増加する。パフォーマンスリグレッションは通常のユニットテストでは検出できません — ベンチマークとプロファイリングが必要です。
| リグレッションの種類 | 例 | 検出方法 |
|---|---|---|
| ローカル | ソートの破損 | ユニットテスト |
| リモート | DBスキーマ変更 | 統合テスト |
| 副作用 | 古いキャッシュ | E2Eテスト |
| パフォーマンス | 応答の遅延 | ベンチマーク |
第一の原因はコードの結合です。モジュールが互いに依存すればするほど、1つの変更が別のモジュールでリグレッションを引き起こす可能性が高くなります。古典的なアンチパターン:God Object(すべてを行うオブジェクト)、Shotgun Surgery(1か所の変更に数十か所の編集が必要)、循環依存。結合度を下げることはアーキテクチャの問題です:SOLID原則、依存性注入、ヘキサゴナルアーキテクチャ。
第二の原因は、変更された機能に対するテストの欠如です。コードがテストでカバーされていない場合、開発者はQAやユーザーからしかリグレッションを知ることができません。Google Testing Blogによると、テストカバレッジが75%以上のプロジェクトは、25%未満のプロジェクトに比べてリグレッションが5分の1です。TDD(テスト駆動開発)は、テストが「時間ができたら」ではなくコードの前に書かれることを保証します。
第三の原因は人的要因です。開発者が関連機能を知らない、すべての依存関係を理解していない、または単に急いでいる。理由はコードベースの知識共有の不足です。解決策:他のモジュールの開発者を含むコードレビュー、ペアプログラミング、アーキテクチャの文書化。プロジェクトのバスファクターは、文書化されたアーキテクチャ上の決定の数に反比例します。
リグレッションテストとは、各変更後に既存のテストを再実行して、古い機能が壊れていないことを確認するプロセスです。新しい変更が既存のコードを損なっていないことを保証する唯一の方法です。リグレッションテストなしでは、すべてのリリースは宝くじのようなものです。開発者は何も壊していないことを願いますが、確認できません。
手動リグレッションテストは最もコストがかかる非効率なアプローチです。プロジェクトが成長するにつれて、リグレッションテストシナリオの数は線形に増加しますが、手動実行時間は指数関数的に増加します。2~3年の開発後、手動リグレッションには2~3週間かかる可能性があり、頻繁なリリースを不可能にします。唯一の解決策は自動化です。
自動化されたリグレッションテストは、テストピラミッドに従ってレベルに分類されます:
Google Testing Blogによると、理想的な比率はユニットテスト70%、統合テスト20%、E2Eテスト10%です。この比率から逸脱すると、リグレッションテストの効果が低下します。E2Eテストが多すぎるとパイプラインが遅くなり、ユニットテストが少なすぎるとマイクロバグが見逃されます。
第一の戦略は完全リグレッションです。プロジェクトのすべてのテストを実行します。最も信頼性が高いが最も遅いアプローチです。小規模プロジェクト(テスト数10,000まで、実行時間30分未満)に適しています。大規模プロジェクトでは、完全リグレッションに数時間かかる可能性があり、CI/CDパイプラインが非現実的になります。
第二の戦略は選択的リグレッションです。変更されたコードに関連するテストのみを実行します。コードの依存関係グラフを使用して関連性を判断します。ツール:Bazel(Google)、Nx(JavaScript)、sbt(Scala)。選択的リグレッションは実行時間を60~80%節約しますが、正確な依存関係グラフの構築が必要です — エラーはリグレッションの見逃しにつながります。
第三の戦略は優先順位付きリグレッションです。すべてのテストを優先順位でランク付けします:クリティカルパス(最も重要なユーザーシナリオ)、高リスク(バグ履歴のあるコード)、変更コード(変更の影響を受けるコード)。最も優先度の高いテストから実行されます — それらが合格すれば、開発者は迅速なフィードバックを得られます。時間枠付き実行:クリティカルテストは10分でチェックされ、残りはバックグラウンドで実行されます。
最初で最も重要なステップは、テストを書く文化です。各変更には、変更が機能することを確認するテストと、何も壊れていないことを確認するテストを伴う必要があります。TDD(テスト駆動開発)が最良の結果をもたらします。開発者はまず失敗するテストを書き、次にそれを合格させるコードを書きます。これにより、テストがコードより先に存在することが保証されます。
第二のステップは、テストの強制実行を伴うCI/CDパイプラインです。すべてのテストが合格するまで、プルリクエストはマージできません。緊急時にテストを「スキップ」することはできません — 緊急の変更は迅速化されたが必須のテストスイートを通過します。Google DevOps Researchによると、必須のCI/CDを導入しているチームは、本番環境でのリグレッションが3分の1です。
第三のステップは本番環境の監視です。最高のテストでもリグレッションに対する100%の防御は保証できません。可観測性ツール(Sentry、Datadog、New Relic)は、各デプロイ後に主要なメトリクス(エラー率、レイテンシ、スループット)を追跡する必要があります。しきい値を超えた場合の自動ロールバックは、リグレッションが本番環境に到達した場合のセーフティネットです。
第四のステップはリグレッションの考え方でのコードレビューです。レビューアは「この変更によって他のどのモジュールが壊れる可能性があるか?」と自問する必要があります。コードが正しいことを確認するだけでは不十分です — 関連機能を損なわないことを確認する必要があります。コードレビューのチェックリストには「関連モジュールのリグレッションチェック」項目を含める必要があります。
よくある質問
リグレッションは以前は存在しなかったバグです。通常のバグは機能作成時から存在していた可能性があります。リグレッションは常に特定の変更に関連しています — これにより、git bisectを使用して原因を特定できます。
git bisectを使用します。すべてが正常に動作していたコミットと、壊れたコミットを指定します。Gitが履歴を二分探索し、リグレッションを引き起こしたコミットを見つけます。これは数千のコミットがある大規模プロジェクトでも機能します。
確定的な数はありませんが、経験則があります。主要なユーザーフローのカバレッジは100%、全機能のカバレッジは少なくとも70%であるべきです。量より質が重要です。エッジケースをチェックする1つのテストは、ハッピーパスの10のテストよりも価値があります。
はい、それはインフラストラクチャリグレッションと呼ばれます。OSの更新、データベースバージョンの変更、SSL証明書の更新、Webサーバー設定の変更が、動作していたコードを壊す可能性があります。IaC(Infrastructure as Code)とインフラストラクチャテスト(Test Kitchen、Terratest)は、そのようなリグレッションの検出に役立ちます。
1つのクリティカルユーザーフローから始めます。最も重要なシナリオ(ログイン、チェックアウト)の自動テストを書きます。デモでテストがリグレッションを捕捉する様子を示します。チームが利点を認識したら、徐々にカバレッジを拡大します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。