BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル モダンなCI/CDパイプラインのための効果的なテスト自動化アプローチを考える

モダンなCI/CDパイプラインのための効果的なテスト自動化アプローチを考える

キーポイント

  • セキュリティなどの領域ではSHIFT LEFTが流行っているが、CI/CDパイプラインでより良いテスト自動化を実現するためにも欠かせない。
  • SHIFT LEFTすることで、テスト容易性の設計を先行させ、テスト専門家を 単体テストの早い段階で関与させることができて、良い結果につながる。
  • CI/CDパイプラインのためにすべてのテストを自動化する必要はない。かわりにCI/CDの実行時間を最小限に抑えながら最良の価値を提供するテストに集中する。
  • パイプライン外のテストはスケジュールに基づいて実行され、メインのパイプラインを混乱させたり遅くしたりすることはない。
  • 効率的で効果的なテストを書くためには、優れたテスト設計の原則に精通する。

原文リンク(2023-05-31)

CI/CDの普及はソフトウェアテストの世界に大きな影響を与えた。開発者はソフトウェアのアップデートが成功したかどうかを迅速にフィードバックするパイプラインを必要としており、多くのテストチームは既存のテスト自動化のアプローチを再検討し品質を維持しながら高速にデリバリーする方法を探している。テストの世界ではこの2つの要素がしばしば矛盾する。というのも目的のテストカバレッジをできるだけ達成しようとするテスターにとって時間はときに最大の敵になるからだ。

ではチームはこの大きな変化にどう対処すればパイプラインが迅速にフィードバックを返し続けて、高品質な自動テストを維持できるのか?
重要なのは、解決策が技術的ではなく文化的なものであること。つまりテストフレームワークを技術的に強化するのではなくテストに対するアプローチを変える必要があるのだ。

SHIFT LEFTを推進する

おそらく、もっともわかりやすい解決への道はSHIFT LEFTすることだろう。"SHIFT LEFT"(テストを開発サイクルの早い段階、主に設計と 単体テストのレベルに移す)という考え方は、すでに多くの組織で推奨されていて業界では一般的になりつつある。 単体テストに強いこだわりを持つことはコードを迅速にテストし迅速なフィードバックを提供する良い方法だ。なにしろ単体テストは非常に短時間で実行されるし(コンパイル中に実行できて、かつシステムとの統合は不要なので)、ただしく運用すれば優れたテストカバレッジを得られるのだ。

多くのテスターが単体テストという概念を敬遠するのは、コードのごく小さな構成要素に対してテストを書くことになり、重要なことを見落とす危険性があるからだ。これは、単体テスト自体の問題というよりも、むしろプロセスの見通しが悪いことや、単体テストへの理解不足が引き起こす恐怖が原因だろう。 単体テストの基礎を抑えればCIパイプラインでコードをビルドする際に迅速にテストを実行できるため有効だ。できるだけ多くの単体テストを書いて、あらゆるタイプのシナリオをカバーすることは有意義なはずである。

最大の問題は、多くのチームがそれをただしく行う方法を知らないことだ。まず、単体テストは単に印をつけるチェックボックスのように扱うのではなく、テスターが通常運用するような適切な分析とテスト設計へのコミットメントでもってアプローチされるべきだ。そしてこれは単体テストを開発者だけに委ねるのではなく、テスターもプロセスに参加してもらうべきだということを意味する。たとえテスターがコーディングに強くなくても、テストにおいてどのパラメータを見るべきか、また後でテストする統合機能に対して正しい結果を出すために、適切なアサート箇所の検討を支援できる。

テストの専門家を単体テストのアプローチに関与させないということは、単体テストが重要な検証領域を見逃す可能性があるということだ。このため、多くのテスターが単体テストを酷評しているのを耳にすることがある。単体テストが効果的でないからというわけではなく、単に正しいシナリオをカバーできていないことが多いからだ。

テスターを早期に参加させることの2つ目の利点は、単体テストに可視性を持たせることだ。テスターは単体テストですでにカバーされていることを知らずに再度テストしてしまい、チームのテストは重複してしまう。おそらくここで発生する無駄な時間(およびコスト)はかなり大きい。分離した検証するべきではない、とまでは言わないが、シナリオでカバーしているならば多重に実行する必要はないだろう。そのぶんテスターは探索的テストに集中すればよいし、他の方法ではカバーできないエッジケースの統合テストを自動化すれば良い。

設計と準備の重要性

これを効果的にすすめるには相当に意識した努力と設計が必要だ。単体テストを重視してテストシナリオを適切に書くためにテスト分析に強い人を雇えばいい、といった簡単な話ではない。適切なテストのためにユーザーストーリーや要件を具体的にしなければならないし、ユーザーストーリーがハイレベルになり、技術的ではなくユーザーレベルの詳細部分にしか、目が向かなくなることも起こりうる。各機能がどのように動作して対応する依存関係と相互作用するのかを明確にし、適切な単体テストを実施しなければならないのだ。

テストコミュニティから単体テストに降りかかる批判は、単体テストの統合性の低さに向けられることが多い。ある機能が単独で動作するからといって、その機能が依存先のなにかと連携して動作するとは限らない。テスターが作業の初期に不具合をたくさん発見するのは、このためであることが多い。とはいうものの、詳細な仕様があれば正確なモッキングが可能になり、単体テストが現実的なふるまいをすることで良い結果を得られることがある。"モック"された機能の中には、正確に把握・設計されていないものが常に存在するが、早期に十分な検討することで、このような手戻りを大幅に削減できるのである。

設計は単体テストだけではない。パイプラインで直接テスト自動化を実行する際の最大の壁は、巨大な統合システムを扱うチームがコードをデプロイした後に、テストと自動化の作業を開始することだ。これは開発プロセスにおける大切な時間の浪費にほかならない。後になってから発生する問題はあるかもしれないが、開発者が近くの席でコーディングしている、その間にテスターが自動テストを書くための詳細情報が、すぐそばにあるのだから。

手動による検証、探索的テスト、実際にソフトウェアを操作することがいけないということではない。これらはテストプロセスの重要な部分であり、ソフトウェアが希望通りの動作をすることを保証する重要なステップでもある。またこれらのアプローチは設計の不具合を見つけるのにも効果的だ。しかし統合テストを自動化することでプロセスを効率化できるのも事実である。そしてテストを初期パイプラインに組み込むことでテストチームが関与することなく、開発チームに不具合を迅速にフィードバックし、納品する製品の全体的な品質を向上できるのである。

何をテストするべきなのだろう?

最良のテスト結果を得るための設計やSHIFT LEFTの具体的なアプローチについて、いくつか話題にした。しかしながらすべてのテストを自動化は不可能である。なぜならCI/CDパイプラインの実行時間が非常に長くなるからであり、それゆえに、どのテストシナリオを単体テストまたは統合テストに組み込むべきかを知ることは、不要なテストの重複を軽減するために重要なのである。

この話に入る前に、重複排除が目的ではあるが適切なカバレッジレベル達成のためには、テスト間で一定レベルの重複が常に存在し得ることを言及しておきたい。可能な限り重複を減らしたいところだが、カバレッジ達成の良い方法が見つからないならば、重複に甘んじた方が無難な場合もあるのだ。

単体テストの対象領域を考える

パイプラインを構築する場合、単体テストと静的解析は、コードのビルド中に評価できるため、一般的にパイプラインのCI部分に該当するはずだ。

エントリーポイント、エグジットポイント(Entry and exit points):すべてのコードは入力を受け取り、そして出力する。まずもって単体テストの検証対象は、コードが受け取るすべてのものと、ただしく出力することである。システム内の各コードを流れるものすべてをキャッチすることで、統合テストのときに発生する不具合を大幅に減らすことができるのだ。

機能性の分離(Isolated functionality):ほとんどのコードは統合されたレベルで動作するが、内部でその処理を完結できる関数も多い。このようなコードは単体テストに限定し、チームはこれらのコードの単体テストカバレッジを100%にすることを目標にするべきである。私はマイクロサービス・アーキテクチャに関わるときに、認証機能や計算処理に依存関係がない分離したサービスをとてもよく見かける。これは追加の統合テストを必要とせず、単体テストが可能であることにほかならない。

境界値の検証(Boundary value validations):UIやAPIから入力されるか、またはコードから直接入力されるかに関わらず、有効な引数受け取った場合でも、無効な引数を受け取った場合でもコードのふるまいは同じである。この多くは単体テストでカバーできるため、テスターが網羅的なシナリオを作成する必要はない。

データの組み合わせを明確にすること(Clear data permutations):データの入力と出力が明確であれば、そのコードやコンポーネントは単体テストの理想的な候補になる。しかしながら、複雑なデータの組み合わせがテスト対象ならば統合テストで取り組むのが良いだろう。なぜなら複雑なデータはモックが難しく、処理に時間がかかり、コーディングパイプラインを遅らせることが多いからだ。

セキュリティとパフォーマンス(Security and performance):負荷テスト、性能テスト、セキュリティテストの大部分は統合テストで行われるが、これらは単体テストでも実行可能だ。コードは無効な認証やリダイレクト、またはSQL(またはコード)インジェクションを処理して効率的にコードを送信する必要がある。これらを検証するための単体テストは作成可能だろう。なぜなら、システムのセキュリティとパフォーマンスは、もっとも弱い部分に依存するからである。したがって弱点を確認することは良い出発点になるのである。

統合テストの対象領域を考える

結合テストは、通常コードを全体環境にデプロイした後に実行されるテストだが、永続的な環境である必要はなく、コンテナの活用も視野に入れるべきだ。しかし多くのチームがこの段階ですべてのテストを試みるのを見かけたことがある。言わずもがなであるが、これはパイプライン実行時間の甚だしい増加につながるし、また毎日定期的な本番環境へのデプロイを想定しているのならば好ましくないことである。

重要なのは単体テストが満足にカバーできそうな部分のみをテストし、同時にテスト設計全体では機能性と性能に重点を置くことだ。この記事の後半で紹介するいくつかの設計原則は、その助けとなるだろう。

正常系シナリオを中心に(Positive integration scenarios):統合したシステムがただしくふるまうように自動化する必要があるのは変わらない。しかし、異常系の検証は単体テストで検証可能な特定の出力がトリガーとなることが多いため、度の過ぎた異常系の検証に傾倒しないことがポイントだ。それよりもシステムの統合が正常に行われることを検証することに集中してもらいたい。

フロントエンドよりバックエンドをテストする(Test backend over frontend):可能であれば、フロントエンドのコンポーネントよりもバックエンドのコンポーネントに集中して自動化を推進してほしい。ユーザーはフロントエンドをよく使うかもしれないが、通常、フロントエンドに機能的な複雑さはない。そして、バックエンドテストは、テスト自動化の実行速度と品質を向上するのである。

セキュリティ(Security):よく見られる間違いは、セキュリティテストの大部分をセキュリティスキャンに頼り、ソフトウェアに対して実行するペネトレーションテストを自動化しないことだ。パイプライン上に組み込んで実行できないペネトレーションテストもあるが、多くは組み込み可能であり、特にアクセスや決済、個人情報を扱う機能では重要性を考慮して、定期的に自動実行すべきである。

CI/CDパイプラインに組み込むべきではない自動テストはあるのか?

自動化に関しては、何を自動化するかだけでなく、何を自動化しないか、あるいは自動テストがあったとしてもそれが常にCI/CDパイプラインに組み込まれるわけではないことを頭の隅に置いておくべきである。また可能な限りSHIFT LEFTしてこれらのテストを減らすことが目標ではあるものの、一部のアーキテクチャでは必ずしも実現できるわけではなく、一定のテストカバレッジを満たすために何らかの追加レベルの検証が必要となる場合があることにも留意してほしい。

これはテストを自動化しパイプラインに組み込んではいけないという意味ではない。CI/CDプロセスから切り離して、スケジュール化された実行構成に組み込んで日常的に実行するべきであり、デプロイされるコードにはしない方が良いという意味である。

データが多く複雑なE2Eテスト(End-to-end tests with high data requirements):複雑なデータシナリオのテストはパイプラインの外側のテスト環境で実行するべきだ。これらのテストは自動化できるが、パイプラインで定期的に実行するには複雑すぎたり、特殊すぎたりすることが多く、実行と検証に時間を要するため、パイプラインには適していない。

ビジュアルリグレッションテスト(Visual regression):機能テスト以外では、サイトのUIに対してビジュアルリグレッションテストを頻繁に行い、さまざまなデバイス、ブラウザ、および解像度で一貫した外観の検証をすることが重要だ。これは、しばしば見落とされがちなテストの重要な側面である。とはいえ実際の機能的なふるまいを検証しているわけではないので、主要なCI/CDパイプラインの外で実施するのが最良だが、メジャーリリースやUIアップデートの前には必須となるだろう。

ミューテーションテスト(Mutation testing):ミューテーションテストは、単体テストのカバレッジをチェックし、コード内のさまざまな決定を調整して何を見逃したのかを確認できる素晴らしいテスト技法だ。しかしながら、非常に時間を要する技法であるためパイプラインの一部を構成するのではなく、レビュープロセスの一部として行うのが良いだろう。

ロードテスト、ストレステスト(Load and stress testing):コードのさまざまな部分の性能をテストすることは重要だが、パイプラインの中でシステムに何らかの負荷やストレスを与えることは避けたいものだ。これをうまくやるには、専用の環境とテスト対象のアプリケーションの限界にストレスを与えるような特定の条件が必要だ。そして、パイプラインに組み込んで実行するような代物ではない。

効果的なテストの設計

ここまでの話で、カバレッジの高い単体テストに強く依存しながら、高い品質のために広い領域をカバーするテストが必要であることは明らかになった。特にCDレベルでは、時間のかかる統合テストがコード展開後に実行されるため、パイプラインの実行にかなりの時間を要するというリスクが常にあるのだ。

ただし、これを効果的にするテスト設計の手法は存在する。不要なテストの自動化は時間の無駄だが、非効率的に書かれたテストの自動化も同様に無駄だ。そして最大の問題はテスターがテスト自動化の効率性を十分に理解しておらず、プロセッサやメモリ効率が向上する方法を探すよりも、実行することに集中しがちなことだ。

すべてのテストがうまく機能するための秘訣はシンプルであることだ。アクションを実行し、レスポンスを得る、それだけである。自動テストは複雑であってはならない。だからテストを設計する際には、この点にこだわることが重要だ。

次に、テスト設計の際に従うべき重要なことや、シンプルでパフォーマンスの高い状態でテストを維持してくれることをまとめた属性リストを紹介しよう。

1.テストに名前をつける(Naming your tests)

テストの命名は重要視されないかもしれないが、保守性という点では重要である。テストの名前はテストの機能や実行速度とは関係ないかもしれないが、そのテストが何をするものなのかを他の人が知るのに役立つ。そのためテストに失敗したときや、何かを修正する必要があるときに、メンテナンスのプロセスをより迅速に行える。これは、パイプラインに組み込んだ何千ものテストに向き合うときに重要となる。

テストは、コードが動作することを確認するだけでなく、ドキュメントを提供するためにも有用だ。単体テストのスイートを見るだけで、コードのふるまいを推測できるはずだ。さらにテストが失敗した場合、どのシナリオが期待にそぐわなかったかを正確に把握できる。

テストの名前は3つの部分から構成されるべきである。

  • テストされるメソッドの名称
  • テストするシナリオの内容
  • シナリオが実行されたときに期待される行動

このような命名規則を用いることで、テストやコードが何をすべきかを簡単に特定ができるし、デバッグを高速化できる。

2.テストの内部構成を整える(Arranging your tests)

読みやすさは、テストを書く上でもっとも重要な要素だ。いくつかのステップを組み合わせてテストのサイズを小さくすることは可能かもしれないが、第一の目標は、できるだけ読みやすいテストにすることである。シンプルで機能的なテストを書くための一般的なパターンは「配置、作用, 検証」で、その名の通り3つの主要なアクションで構成される。

  • Arrange: オブジェクトを配置する。オブジェクトを作成し、設定することで、意図したテストに対応できるようにする。
  • Act: オブジェクトに作用させる。
  • Assert: 何かが期待通りであることを検証する。

これらのアクションをテスト内で明確に分離することで、次のことが強調される。

  • コードやテストを呼び出すために必要な依存関係
  • コードがどのように呼び出されるのか
  • 何を検証しようとしているのか

これによりテストは書きやすく、理解しやすく、メンテナンスしやすくなる。また単純な操作を毎回実行するため、全体的なパフォーマンスも向上するのである。

3.最低限合格するテストを書く(Write minimally passing tests)

自動テストを書いている人は、複数の異なる動作に対応できる複雑なコーディング技術を採用しがちだが、テストの世界では複雑さをもたらすだけである。 テストを合格にするための情報よりも過剰な情報を含むテストは、エラーを引き起こす可能性が高く、テストの意図がわかりにくくする可能性がある。例えばモデルに余計なプロパティを設定したり、必要ないのにゼロ以外の値を使用したりすると、本来検証したいことが損なわれてしまうだけである。テストを書くときは、動作に焦点を当てたいものだ。そのためには、使用する入力はできるだけシンプルにする必要があるのだ。

4.テストにロジックを含めない(Avoid logic in tests)

テストスイートにロジックを導入すると、人為的なミスや誤った結果によってバグを発生させる可能性が飛躍的に高くなる。なぜならテストが機能するという高いレベルの信頼性を維持するべきだからだ。そうでなければテストは信頼されず何の価値も提供しなくなるのである。

テストを書く際は、手動による文字列の連結やif、while、for、switchなどの論理条件を避けるようにしたい。同様にあらゆる形式の計算も避けるべきだ。テストは簡単に識別できる入力と明確な出力に依存するべきである。そうでない場合、特定の基準に基づいて簡単に欠陥が生じてしまう。そしてプロダクションコードのロジックが変更されると、テストのロジックも変更するためメンテナンスが必要になるのである。

ここでもう一つ重要なのは、パイプラインのテストは迅速に実行されるべきで、テストに含まれたロジックは多くの処理時間を要する傾向があることを忘れてはいけないということだ。もしかすると些細なことに思えるかもしれない。しかし、数百のテストがあれば、塵が積もって影響を及ぼすこともあるのだから。

5.可能な限りモックとスタブを使う(Use mocks and stubs wherever possible)

モックやスタブの多用はアプリケーションの真の統合時のふるまいをあらわしていないとみなされがちなため、テスターの多くは嫌がるかもしれない。これらは自動化やE2Eテストには有効だが、パイプラインの実行には適していない。パイプラインの実行が遅くなるだけでなく外部機能が動作していなかったり変更と同期していなかったりするために、テスト結果が不安定になりやすい。

テスト結果の信頼性を高め、テスト作業をよりコントロールし、カバレッジを向上させる最良の方法は、テストフレームワークにモッキングを組み込み、外部関数が代わりに行う複雑なデータパターンをインターセプトするスタブに依存することだ。

6.セットアップとティアダウンにヘルパーメソッドを優先させる(Prefer helper methods to Setup and Teardown)

単体テストフレームワークでは、テストスイート内の各単体テストの前にSetup関数が呼び出される。それぞれのテストは一般的にテストを立ち上げて実行するために異なる要件を持っている。残念ながらSetupは各テストで全く同じ要件を使用することを強制する。これを便利と考える人もいるかもしれないが一般的にはテストが肥大化し読みづらくなる一因である。テストに似たようなオブジェクトやステートを要する場合はSetupやTeardownメソッドよりも、既存のヘルパーメソッドを使用したい。

次の点において役に立つだろう。

  • すべてのコードが各テストの中から見えるので、テストを読むときの混乱が少なくなる。
  • 与えられたテストに対して、多すぎたり少なすぎたりする可能性が少ない。
  • テスト間で状態を共有することで、テスト間に不要な依存関係が発生する可能性が低くなる。

7.複数のアサートは避ける(Avoid multiple asserts)

テストケースに複数のアサーションを導入する場合、すべてのアサーションの実行は保証されていない。これはアサーションでテストが失敗・中断して残りのテストが実行されない可能性が高いからだ。単体テストでアサーションが失敗すると、それ以降のテストは、たとえ失敗していなくても自動的に失敗したとみなされる。その結果、不具合の発生箇所がわからなくなり、デバッグ時間の浪費につながるのである。

テストを書くときは、1つのテストに1つのアサートしか含めないようにしてもらいたい。こうすることでどこで失敗したのか、なぜ失敗したのかを正確に突き止めることが容易になる。チームでは高いカバレッジを実現するためにできるだけ少ないテストを推奨するという過ちを犯しがちだが、結局のところ将来のメンテナンスが酷いことになるだけなのだ。

これはテストの重複を排除することにもつながる。パイプラインでの重複したテストの実行を避けるために、テスト内容の可視化を推進してチームがこの目的を確実に達成できるようにする。

8.テストをプロダクションコードと同等に扱う(Treat your tests like production code)

テストコードは本番環境では実行されないかもしれないが、プロダクションコードと同等に扱われるべきだ。つまり定期的に更新され維持されるべきであるということ。テストを書いてそれですべてが完了したと思ってはいけない。テストを機能的かつ健全さを維持するための作業し、同時にすべてのライブラリと依存関係をアップデートしつづけるべきである。例えばあなたのチームのプロダクションコードに技術的負債があるのは嫌だろうが、テストコードに技術的負債があるのもこれまた嫌なことだろう。

9.テスト自動化を習慣化する(Make test automation a habit)

さて、最後に紹介するのは設計原則というよりは良いテストの書き方に関するヒントだ。コーディングに関連するすべての事柄と同様に、理論を知るだけでは十分ではなく、うまくなって習慣として身につけるには練習が必要である。したがってこれらのテストプラクティスをただしく自然に感じられるようになるには時間を要するだろう。適切なテストを書くというスキルは非常に過小評価されているが、コードの品質に大きな価値を与えるのだから、そのための努力と労力は確実に価値のあることなのだ。

結論 ~ すべては優れたテスト設計のために

このようにフルスタックでのテスト自動化はパイプラインの中で機能して、不要な中断や遅延をせずに高レベルの回帰カバレッジを提供する。ただし必要なのは効果的に動作するための優れたテスト設計であり、単体テストと自動テストはそのために最大限の価値を発揮するように適切に作成するべきである。

優れたDevOpsテスト戦略には、カバレッジの大部分を担う単体テストの強固な基盤と、残りの自動化作業を促進するモック(またはスタブ)の仕組みで構成され、これによってE2E自動テストの追加がほんの少しだけで済んだパイプラインが求められる。このパイプラインでもって初めて、チームは自分たちのパイプラインテストが要求される品質を適切に満たすことを確信できるようになるのである。

作者について

この記事に星をつける

おすすめ度
スタイル

BT