実用目的で発明されたツールの中でも、ソフトウェアは特に複雑です。 文字をたった 1 つ間違えるだけで、アプリケーション全体が破損することすらあります。 そのため、コードをパブリッシュする前に入念なテストを行うことが欠かせません。 この記事では、2 つの基本的なソフトウェア テストである単体テスト (ユニット テスト) と結合テストについて解説します。また、これらのテストを CI/CD パイプラインに実装することで、コードの検証にかける時間を短縮し、自信を持って新機能をリリースできるようになる理由を説明します。
DevOps チームや開発者は、長い時間をかけてさまざまなソフトウェア テスト手法を導入してきました。 その多くを組み合わせて、さまざまなテスト ピラミッドのバリエーションが構築されています。 単体テストと結合テストは、下図のようにピラミッドの土台 2 つを構成していることがほとんどであり、包括的なテスト戦略を立てるうえでとても重要です。
図では明確に区分けされているものの、現実には、単体テストや結合テストをはじめとするテスト間にはっきりとした境界はありません。 それぞれのテスト カテゴリは排他的というわけではなく、互いを補い合う関係にあります。 したがって、単体テストと結合テストのどちらが重要かと比べ合うのではなく、その両方をCI・継続的インテグレーション パイプラインのどこで使用すべきか考えるのがベストです。 とはいえ、その前に、単体テストと結合テストの基本について確認しておきましょう。
単体テスト(ユニットテスト)とは?
単体テスト (ユニットテスト) とは、アプリケーション全体の中から 1 つの部分 (通常は単一のクラスや関数) だけを完全に取り出して実行するテストです。 テスト対象のコンポーネントに副作用が存在しない状態が理想です。副作用が一切存在しなければ、対象コンポーネントを分離した上で、テストを可能な限り簡単に行えるからです。
しかし、実際には、ここまでのレベルの分離をいつでも実現できるとは限りません。 きれいに分離することができない場合、それだけテストの実行は難しくなります。
また、他の要因によって単体テストでできる範囲が制限される場合もあります。 たとえば private や public といったアクセス修飾子を使用するプログラミング言語では、private 関数だけをテストすることはできません。 ただしこれらの制限は、特別なコンパイラ命令やフラグを使えば回避できることもあります。 それでも回避できない場合には、これらの制限されたヘルパーを単体テストするために、コードに変更を加える必要が生じてきます。
単体テストのメリットは、なんと言ってもそのスピードです。 副作用が存在しないことが前提なので、他のシステムとは完全に独立して直接実行することができます。 単体テストには、基盤オペレーティング システムとの依存関係 (ファイル システム アクセスやネットワーク アクセスなど) を含めないことが理想です。 とは言え、現実にはなんらかの依存関係が存在するものです。 そこで、依存関係をテストのために切り出した状態でテストを行えるようにします。 このプロセスをモッキングといいます。
また単体テストは、テスト駆動開発 (TDD) という先進的なソフトウェア開発プロセスにおいて、重要な位置を占めています。 テスト駆動開発プロセスでは、DevOps の専門家や開発者が実装に先行してテストコードを記述します。 テスト駆動開発のゴールは、まず個々のユニットのロールアウト時の仕様を固めてから、仕様通りに動作する実装を完成させることです。
このような「契約」は魅力的に思えますが、一方で注意すべき欠点があります。 それは、仕様が正確であるとともに、テストコードを書く開発者が実装の少なくとも一部分をコンセプトの観点で把握していなければならないことです。 こうした欠点は、アジャイルの原則に反するものです。
単体テストについての解説はここまでにして、今度は結合テストの特徴について見てみましょう。
結合テストとは?
ここまで説明してきたように、”分離” という特性を持つ単体テストでは対応しきれない機能もあります。 その場合の解決策の 1 つは、アプリケーションの各部分が全体としてどのように連携するかをテストすることです。 この手法を結合テストといいます。
単体テストとは異なり、結合テストでは副作用の存在が前提です。 副作用があった方が良いとされる場合すらあります。
たとえば、結合テストでは、データベースとの接続 (単体テストで言う “依存関係”) を使用して、普段どおりにデータベースを検索し、データを変更します。 したがって、データベースを準備した上で、正しくデータを読み出さなければなりません。 これらの外部リソースも単体テストの場合と同じように “モッキング” するケースがよく見られますが、 これでは制御範囲外にある API により引き起こされた障害がわかりにくくなります。
アプリケーションの実装や特定のユニットの実装を調べることで、見えにくい問題を洗い出すのが結合テストです。 このテストでは、アプリケーションの要素間の連携に潜む欠陥を発見します。 これらの欠陥は追跡や再現が難しいことがあります。
テスト カテゴリの境界は曖昧ですが、結合テストには重要な特性があります。それは、”アプリケーションの複数の要素を扱う” こと。 単体テストでは常に単一ユニット (関数呼び出しなど) からの結果を受け取るのに対し、結合テストでは複数のモジュールやデータソースからの結果を集約するケースもあります。
結合テストでは、アプリケーションの要素をモッキングする必要がありません。 外部システムを置き換えても、アプリケーションは “結合されたまま” 動作します。 この手法は、CI/CD パイプラインにおける検証に便利です。
CI/CD における単体テストと結合テスト
テストは効果的に実行しなければなりません。 テストを自動化する大きなメリットの 1 つが、無人で実行できるということです。 ほとんどの DevOps の原則では、CI/CD パイプラインにおけるテストの自動化は、必須でないにしてもベスト プラクティスとみなされています。
システムで “テストのトリガーが可能かつ必須とされる” 段階はさまざまです。 まず、メイン ブランチのいずれかにコードをプッシュする場合、テストは必ず実行しなければなりません。 この状況はプル リクエストの一環ということもあります。 いかなる場合も、いずれかのテストに失敗したコードが、メイン ブランチにマージされる事態は避けなくてはなりません。
すべてのテストに合格した場合のみコードの変更をデプロイするように、CD ツールを設定しましょう。 この設定の適用対象は、すべての環境にしても、本番環境のみにしてもかまいません。 このようなフェイルセーフがなぜ重要かというと、副作用をきちんと確認しないまま問題の緊急修正をリリースしないようにするためです。 副作用の確認には少し時間を要するかもしれませんが、それに見合うだけの価値はあります。
また、本番環境やその他の環境のリソースに対し、定期的にテストを実行することもお勧めします。 これにより、すべて問題なく動作し続けていることを把握できます。 本番環境の予期せぬ中断を防ぐという観点では、サービスの監視がなおのこと重要です。
CI/CD パイプラインは高速でなければならないため、大半のテストもできる限り短時間に完了すべきと言えます。 多数の単体テストを使用するのが最速ですが、全体として重要なメトリクスはカバレッジと関連性です。
開発チームがプロジェクトで作成すべきなのは、関連するコード パスをすべてカバーした、効率と信頼性に優れるテストです。 これらのテストを CI/CD パイプラインで自動実行することを、チームの最優先事項にしてください。 さまざまなテスト方法を組み合わせれば、テスト カバレッジを強化し、ソフトウェアからバグを可能な限り取り除くことができるでしょう。
おわりに
ソフトウェア開発を成功させるためには、単体テストと結合テストの両方が不可欠です。 それぞれの目的は異なりますが、関連性はあり、どちらか 1 つで済ませることはできません。 お互いがお互いをちょうど良く補い合っているのです。
単体テストの方がすばやく作成できることがほとんどですが、結合テストはその信頼性の高さから、主要な関係者に安心感を与える傾向にあります。 みなさんのアプリケーションが今日も、そしてこれからも確実に動作するよう、両方のテスト カテゴリを活用しましょう。
CI/CD ツールを導入すれば、”変更が行われたとき”、”一定間隔”、”オンデマンドで” などのタイミングでテストを自動実行できます。 実行するテストの数を増やせば増やすほど、より多くのデータを得られます。そして、より多くの方法で、ソフトウェア アプリケーションが本番環境で安定して動作していることを確かめられるようになります。 テスト戦略を自動化すれば、チームの開発スピードを高めると同時に、非効率的でコストのかかる手動プロセスを排除できます。今すぐ無料で CircleCI を始めて、そのメリットをぜひ実感してください。