残った単体テストによるシフトテスト
テストはコードが期待どおりに実行されることを確認するのに役立ちますが、テストの構築に時間と労力がかかると、機能開発などの他のタスクの時間が奪われます。 このコストを考慮すると、テストから最大の価値を引き出すことが重要です。 この記事では、単体テストの価値とシフトレフト テスト戦略に焦点を当てて、DevOps テストの原則について説明します。
かつては専任のテスターがほとんどのテストを作成していましたが、多くの製品開発者は単体テストの作成方法を学習していませんでした。 テストを書くのは難しすぎる、または大変な作業のように思えるかもしれません。 単体テスト戦略が機能するかどうかについての懐疑、不十分に作成された単体テストでの悪い経験、または単体テストが機能テストに取って代わるのではないかという不安がある可能性があります。
DevOps テスト戦略を実装するには、現実的であり、勢いを高めることに重点を置きます。 新しいコードや、きれいにリファクタリングできる既存のコードに対して単体テストを行うことを主張することもできますが、レガシー コードベースではある程度の依存関係を許可することが合理的である可能性があります。 製品コードの重要な部分で SQL が使用されている場合、その層をモックするのではなく、単体テストが SQL リソース プロバイダーに依存できるようにすることが、短期的には進歩するアプローチになる可能性があります。
DevOps 組織が成熟するにつれて、リーダーがプロセスを改善することが容易になります。 変化には多少の抵抗があるかもしれませんが、アジャイル組織は明らかに利益をもたらす変化を重視します。 機能開発を通じて新しい価値を生み出すためにより多くの時間を投資できることになるため、失敗を減らしてテストを高速化するというビジョンを売り込むのは簡単なはずです。
DevOps テストの分類法
テスト分類の定義は、DevOps テスト プロセスの重要な側面です。 DevOps テスト分類法は、個々のテストを依存関係と実行にかかる時間によって分類します。 開発者は、さまざまなシナリオで使用する適切なタイプのテストと、プロセスのさまざまな部分でどのテストが必要かを理解する必要があります。 ほとんどの組織は、テストを次の 4 つのレベルに分類しています。
- L0および L1 テストは、単体テストまたはテスト対象のアセンブリ内のコードのみに依存するテストです。 L0 は、高速なメモリ内単体テストの広範なクラスです。
- L2 は、アセンブリに加えて SQL やファイル システムなどの他の依存関係を必要とする可能性がある機能テストです。
- L3 機能テストは、テスト可能なサービス展開に対して実行されます。 このテスト カテゴリにはサービスの展開が必要ですが、主要なサービスの依存関係にスタブを使用する可能性があります。
- L4 テストは、運用環境に対して実行される 統合テスト の制限されたクラスです。 L4 テストには、完全な製品展開が必要です。
すべてのテストを常に実行するのが理想的ですが、それは現実的ではありません。 チームは、DevOps プロセス内の各テストを実行する場所を選択し、左シフト または右シフト 戦略を使用して、プロセスの前後に異なるテスト タイプを移動できます。
たとえば、開発者はコミット前に常に L2 テストを実行し、L3 テストの実行が失敗するとプル リクエストは自動的に失敗し、L4 テストが失敗するとデプロイメントがブロックされる可能性があることが期待される場合があります。 具体的なルールは組織によって異なる場合がありますが、組織内のすべてのチームに期待を強制することで、全員が同じ品質ビジョンの目標に向かうようになります。
単体テストのガイドライン
L0 および L1 単体テストの厳格なガイドラインを設定します。 これらのテストは非常に高速で信頼性が高い必要があります。 たとえば、アセンブリ内の L0 テストごとの平均実行時間は 60 ミリ秒未満である必要があります。 アセンブリ内の L1 テストごとの平均実行時間は 400 ミリ秒未満である必要があります。 このレベルのテストは 2 秒を超えてはなりません。
ある Microsoft チームは、60,000 を超える単体テストを 6 分以内に並行して実行しています。 彼らの目標は、この時間を 1 分未満に短縮することです。 チームは、次の図のようなツールを使用して単体テストの実行時間を追跡し、許容時間を超えたテストに対してバグをファイルします。
機能テストのガイドライン
機能テストは独立している必要があります。 L2 テストの重要な概念は分離です。 適切に分離されたテストは、実行環境を完全に制御できるため、どのような順序でも確実に実行できます。 テストの開始時に状態を把握しておく必要があります。 1 つのテストでデータが作成され、それがデータベースに残された場合、別のデータベースの状態に依存する別のテストの実行が破損する可能性があります。
ユーザー ID を必要とする従来のテストでは、ID を取得するために外部認証プロバイダーを呼び出していた可能性があります。 この実践にはいくつかの課題が伴います。 外部依存関係の信頼性が低いか、一時的に利用できなくなり、テストが中断される可能性があります。 また、テストによって権限などの ID の状態が変更され、他のテストで予期しないデフォルト状態が発生する可能性があるため、この方法はテスト分離原則にも違反します。 テスト フレームワーク内の ID サポートに投資して、これらの問題を防ぐことを検討してください。
DevOps テストの原則
テスト ポートフォリオを最新の DevOps プロセスに移行できるようにするには、品質のビジョンを明確にします。 チームは、DevOps テスト戦略を定義して実装する際に、次のテスト原則に従う必要があります。
左にシフトして先にテストします
テストの実行には長い時間がかかる場合があります。 プロジェクトが拡大するにつれて、テストの数と種類は大幅に増加します。 テスト スイートが成長して完了までに数時間または数日かかる場合、最後の瞬間に実行されるまでさらに先へ進む可能性があります。 テストによるコード品質の利点は、コードがコミットされてからかなり経ってから実現されます。
長時間実行されるテストでは、調査に時間がかかるエラーが発生する場合もあります。 チームは、特にスプリントの初期段階で、失敗に対する耐性を構築できます。 この許容差は、コードベースの品質を洞察するためのテストの価値を損ないます。 また、長時間実行される直前のテストでは、コードを出荷可能にするために未知の量の技術的負債を支払わなければならないため、スプリント終了の予測に予測不可能性が加わります。
テストを左にシフトする目標は、パイプラインの早い段階でテスト タスクを実行することで、品質を上流に移すことです。 テストとプロセスの改善を組み合わせることで、左にシフトすると、テストの実行にかかる時間と、サイクル後半での障害の影響の両方が削減されます。 左にシフトすると、変更がメイン ブランチにマージされる前にほとんどのテストが完了します。
コードの品質を向上させるために特定のテスト責任を左に移すだけでなく、チームは他のテストの側面を右に、または DevOps サイクルの後半に移して、最終製品を改善することができます。 詳細については、「実稼働環境でテストするために右にシフトする」を参照してください。
可能な限り低いレベルでテストを作成する
さらに単体テストを作成します。 外部依存関係が最も少ないテストを優先し、ほとんどのテストをビルドの一部として実行することに重点を置きます。 アセンブリと関連するテストが終了したらすぐに、アセンブリの単体テストを実行できる並列ビルド システムを検討してください。 このレベルでサービスのすべての側面をテストすることは現実的ではありませんが、原則として、より重い機能テストと同じ結果が得られる場合は、より軽い単体テストを使用します。
検査の信頼性を目指す
信頼性の低いテストは組織的に維持コストがかかります。 このようなテストは、自信を持って変更を加えることが困難になるため、エンジニアリング効率の目標に直接反するものになります。 開発者はどこでも変更を加えることができ、何も壊れていないという確信をすぐに得ることができる必要があります。 信頼性のために高い基準を維持してください。 UI テストは信頼性が低い傾向があるため、使用しないでください。
どこでも実行できる機能テストを作成する
テストでは、テストを可能にするために特別に設計された特殊な統合ポイントを使用する場合があります。 このような行為が行われる理由の 1 つは、製品自体のテスト可能性の欠如です。 残念ながら、このようなテストは多くの場合、内部知識に依存しており、機能テストの観点からは重要ではない実装の詳細を使用します。 これらのテストは、テストの実行に必要なシークレットと構成を持つ環境に限定されており、通常は運用環境の展開は除外されます。 機能テストでは、製品のパブリック API のみを使用する必要があります。
テスト容易性を考慮した製品の設計
DevOps プロセスが成熟している組織は、クラウド リズムで高品質の製品を提供することが何を意味するのかを完全に把握しています。 機能テストよりも単体テストを優先する方向にバランスを大きく変えるには、チームがテスト容易性をサポートする設計と実装の選択を行う必要があります。 さまざまなコーディング スタイルがあるのと同じように、テストを容易にするために適切に設計され、適切に実装されたコードを構成するものについてはさまざまな考え方があります。 原則として、テスト容易性を考慮した設計が、設計とコードの品質に関する議論の主要な部分にならなければならないということです。
テストコードを製品コードとして扱う
テスト コードが製品コードであると明示することで、テスト コードの品質が製品コードの品質と同じくらい出荷にとって重要であることが明確になります。 チームは、製品コードを扱うのと同じ方法でテスト コードを扱い、テストとテスト フレームワークの設計と実装にも同じレベルの注意を払う必要があります。 この取り組みは、構成とインフラストラクチャをコードとして管理することに似ています。 完全を期すためには、コード レビューでテスト コードを検討し、それを製品コードと同じ品質基準に保つ必要があります。
共有テストインフラストラクチャを使用する
テスト インフラストラクチャを使用して信頼できる品質の信号を生成するハードルを下げます。 テストをチーム全体の共有サービスとして捉えます。 単体テスト コードを製品コードと一緒に保存し、製品と一緒にビルドします。 ビルド プロセスの一部として実行されるテストは、Azure DevOps などの開発ツールでも実行する必要があります。 ローカル開発から本番までのあらゆる環境でテストを実行できれば、テストは製品コードと同じ信頼性を持ちます。
コード所有者にテストの責任を持たせる
テスト コードは、リポジトリ内の製品コードの隣に存在する必要があります。 コンポーネント境界でコードをテストする場合は、コンポーネント コードを作成する人にテストの責任を押し付けます。 コンポーネントのテストを他人に依存しないでください。
ケーススタディ: 単体テストによるシフトレフト
Microsoft チームは、従来のテスト スイートを最新の DevOps 単体テストとシフトレフト プロセスに置き換えることを決定しました。 次のグラフに示すように、チームは 3 週間ごとのスプリントで進捗状況を追跡しました。 グラフはスプリント 78 ~ 120 をカバーしており、これは 126 週間にわたる 42 のスプリント、つまり約 2 年半の取り組みを表しています。
チームはスプリント 78 で 27,000 個のレガシー テストから開始し、S120 でレガシー テストがゼロに達しました。 一連の L0 および L1 単体テストが、古い機能テストのほとんどを置き換えました。 一部のテストは新しい L2 テストに置き換えられ、古いテストの多くは削除されました。
完了までに 2 年以上かかるソフトウェアの開発では、プロセス自体から学ぶべきことがたくさんあります。 全体として、2 年間にわたってテスト システムを完全にやり直す作業は、巨額の投資でした。 すべての機能チームが同時に作業を行ったわけではありません。 組織全体の多くのチームがすべてのスプリントに時間を投資しており、一部のスプリントではそれがチームの仕事のほとんどでした。 移行にかかるコストを測るのは難しいですが、チームの品質とパフォーマンスの目標にとっては交渉の余地のない要件でした。
作業の開始
当初、チームは TRA テストと呼ばれる古い機能テストをそのまま残しました。 チームは、開発者に、特に新機能の単体テストを作成するというアイデアを受け入れてもらいたいと考えていました。 L0 テストと L1 テストの作成をできるだけ簡単にすることに重点が置かれました。 チームはまずその能力を開発し、勢いを高める必要がありました。
上のグラフは、チームが単体テストを作成する利点を認識したため、単体テスト数が早期に増加し始めていることを示しています。 単体テストは保守が容易で、実行が速く、失敗が少なくなりました。 プル リクエスト フローですべての単体テストを実行するためのサポートを得るのは簡単でした。
チームはスプリント 101 まで新しい L2 テストの作成に集中しませんでした。 一方、TRA 検査数は、スプリント 78 からスプリント 101 までに 27,000 件から 14,000 件に減少しました。 一部の TRA テストは新しい単体テストに置き換えられましたが、その有用性に関するチーム分析に基づいて、多くは単純に削除されました。
ソース ツリーでさらに多くのテストが検出され、グラフに追加されたため、TRA テストはスプリント 110 で 2100 から 3800 に跳ね上がりました。 テストは常に実行されていたものの、適切に追跡されていなかったことが判明しました。 これは危機ではありませんでしたが、正直になり、必要に応じて再評価することが重要でした。
高速化
チームが非常に高速で信頼性の高い 継続的インテグレーション (CI) シグナルを取得すると、それは製品品質の信頼できる指標になりました。 次のスクリーンショットは、動作中のプル リクエストと CI パイプライン、およびさまざまなフェーズを通過するのにかかる時間を示しています。
プル リクエストからマージまでには、60,000 個の単体テストの実行を含め、約 30 分かかります。 コードのマージからCIビルドまでは約22分です。 CI からの最初の品質シグナルである SelfTest は、約 1 時間後に送信されます。 次に、提案された変更を使用して製品の大部分がテストされます。 Merge から SelfHost までの 2 時間以内に製品全体がテストされ、変更を実稼働環境に導入する準備が整います。
メトリックの使用
チームは、次の例のようなスコアカードを追跡します。 大まかに言うと、スコアカードは 2 種類のメトリクス (健全性または負債、およびベロシティ) を追跡します。
ライブサイトの健全性メトリクスについては、チームは検出にかかる時間、緩和にかかる時間、チームが運んでいる修理品の数を追跡します。 修復項目とは、同様のインシデントの再発を防ぐためにチームがライブサイトの振り返りで特定する作業です。 スコアカードは、チームが妥当な期間内に修理項目をクローズしているかどうかも追跡します。
エンジニアリングの健全性指標については、チームは開発者ごとにアクティブなバグを追跡します。 チームに開発者あたり 5 つを超えるバグがある場合、チームは新機能の開発前にそれらのバグの修正を優先する必要があります。 チームはまた、セキュリティなどの特別なカテゴリの古いバグも追跡しています。
エンジニアリング速度メトリクスは、継続的インテグレーションおよび継続的デリバリー (CI/CD) パイプラインのさまざまな部分の速度を測定します。 全体的な目標は、DevOps パイプラインの速度を向上させることです。つまり、アイデアから始めて、コードを運用環境に導入し、顧客からデータを受け取ることです。