単体テスト(コンポーネントテストまたはモジュールテストとも呼ば れる)は、分離されたソースコードをテストして期待される動作を検証するソフトウェアテストの一形態である。 [ 1 ]
単体テストとは、統合レベルまたはシステムレベルでのテストとは対照的に、ユニットレベルで実行されるテストのことです。[ 2 ]
ユニットテストは、大規模なソフトウェアシステムの小さな部分を個別にテストする原則として、ソフトウェアエンジニアリングの初期の頃にまで遡ります。1956 年 6 月、米国海軍のデジタル コンピュータ向け高度プログラミング手法に関するシンポジウムで、HD ベニントンはSAGEプロジェクトを発表しました。これは仕様ベースのアプローチを採用しており、コーディング フェーズの後に、コンポーネント サブルーチンを仕様に照らして検証する「パラメータ テスト」が行われ、その後、部品を組み立てる「アセンブリ テスト」が行われました。[ 3 ] [ 4 ]
1964年には、マーキュリー計画のソフトウェアについても同様のアプローチが説明されており、異なるプログラマーによって開発された個々のユニットは、統合される前に「ユニットテスト」を受けました。[ 5 ] 1969年には、テスト手法はより構造化され、ユニットテスト、コンポーネントテスト、統合テストによって、個別に書かれた個々の部分と、それらがより大きなブロックに段階的に組み立てられることをまとめて検証するようになりました。[ 6 ] 1960年代後半に採用されたMIL-STD-483 [ 7 ]やMIL-STD-490 などのいくつかの公開規格は、大規模プロジェクトにおけるユニットテストの幅広い受け入れにさらに貢献しました。
当時、単体テストは対話型[ 4 ]または自動化[ 8 ]で、 コード化されたテストまたはキャプチャおよび再生テストツールを使用していました。1989年、ケント・ベックは「Simple Smalltalk Testing: With Patterns」でSmalltalkのテストフレームワーク(後にSUnitと呼ばれる)について説明しました。1997年、ケント・ベックとエーリッヒ・ガンマは、 Java開発者の間で人気となった単体テストフレームワークであるJUnitを開発してリリースしました。[ 9 ] Googleは2005年から2006年頃に自動テストを採用しました。[ 10 ]
ユニットとは、テスト対象システム(SUT)が示す単一の動作として定義され、通常は要件に対応します。ユニットは、単一の関数またはモジュール(手続き型プログラミングの場合)または単一のメソッドまたはクラス(オブジェクト指向プログラミングの場合)に対応する場合がありますが、関数/メソッドやモジュール/クラスが必ずしもユニットに対応するとは限りません。システム要件の観点からは、システムの境界のみが関連しているため、外部から見えるシステム動作へのエントリポイントのみがユニットを定義します。[ 11 ]
単体テストは、手動で実行することも、自動テスト実行によって実行することもできます。自動テストには、テストを頻繁に実行できる、人員コストをかけずにテストを実行できる、一貫性があり再現性の高いテストを実行できる、といった利点があります。
テストは、多くの場合、テスト対象のコードを記述・修正するプログラマー自身によって実施されます。単体テストは、コード記述プロセスの一部とみなすことができます。
開発段階で、プログラマーはユニットの正確性を検証するために、基準、つまり良好な結果であることがわかっているものをテストに組み込むことがあります。
テスト実行中、フレームワークは基準を満たさなかったテストをログに記録し、概要として報告します。
このため、最も一般的に用いられるアプローチは、テスト - 関数 - 期待値です。
ソフトウェアエンジニアリングにおいて、テストケースとは、特定のプログラムパスを実行したり、特定の要件への準拠を検証したりするなど、特定のソフトウェアテストの目的を達成するために実行される単一のテストを定義する入力、実行条件、テスト手順、および期待される結果の仕様です。[ 12 ]テストケースは、無秩序ではなく体系的なテストの基盤となります。テスト対象のソフトウェアの望ましいカバレッジを実現するために、一連のテストケースを構築できます。正式に定義されたテストケースにより、ソフトウェアの連続するバージョンに対して同じテストを繰り返し実行できるため、効果的で一貫性のある回帰テストが可能になります。[ 13 ]
パラメータ化テストとは、複数の異なる入力値を用いてテストを実行できるように、一連の値を受け入れるテストのことです。パラメータ化テストをサポートするテストフレームワークは、パラメータセットをエンコードし、各セットを用いてテストを実行する方法を提供します。
パラメータ化されたテストを使用することで、テストコードの重複を減らすことができます。
パラメータ化されたテストは、 TestNG、JUnit、[ 14 ] XUnit、NUnit、およびさまざまなJavaScriptテストフレームワークでサポートされています。
単体テストのパラメータは手動でコーディングすることも、場合によってはテストフレームワークによって自動的に生成することもできます。近年、理論の概念を活用した、より強力な(単体)テストを作成するためのサポートが追加されました。これは、事前に定義された入力セットを使用して同じ実行ステップを実行する通常のパラメータ化テストとは異なり、実行時に生成されたテストデータを使用して同じステップを実行するテストケースです。[ 15 ]
テストコードはテスト対象のコードにアクセスする必要がありますが、テストによって情報隠蔽、カプセル化、関心の分離といった通常の設計目標が損なわれてはなりません。外部APIで公開されていないコードへのアクセスを可能にするには、単体テストをテスト対象のコードと同じプロジェクトまたはモジュール内に配置することができます。
オブジェクト指向設計では、それでもプライベートなデータやメソッドへのアクセスは提供されない場合があります。そのため、単体テストには追加の作業が必要になる場合があります。Javaやその他の言語では、開発者はリフレクションを使用してプライベートなフィールドやメソッドにアクセスできます。[ 16 ]あるいは、内部クラスを使用して単体テストを保持し、テストが囲んでいるクラスのメンバーや属性を認識できるようにすることもできます。.NET Frameworkやその他のプログラミング言語では、部分クラスを使用して、テストがアクセスできるようにプライベートなメソッドやデータを公開することができます。
テストのためだけに作成されたコードが本番コードに残らないようにすることが重要です。C 言語やその他の言語では、コンパイラディレクティブを使用して、このような#if DEBUG ... #endif追加クラスやその他すべてのテスト関連コードをコンパイル時に除外することができます。つまり、リリースされたコードは単体テストされたコードと完全に同じではありません。最終リリースビルドに対して、数は少ないもののより包括的なエンドツーエンドの統合テストを定期的に実行することで、(とりわけ)テストハーネスの要素に微妙に依存する本番コードが存在しないことを保証できます。
プライベートメソッドやプライベートデータをテストすることが賢明かどうかについては、開発者の間で議論があります。プライベートメンバーは単なる実装の詳細であり、変更される可能性があり、テストの数を損なうことなく変更を許可すべきだと主張する人もいます。したがって、パブリックインターフェースまたはサブクラスインターフェース(一部の言語では「保護された」インターフェースと呼ばれています)を介して任意のクラスをテストすれば十分であるはずです。[ 17 ]一方、機能の重要な側面はプライベートメソッドで実装されている場合があり、それらを直接テストすることで、より小さく直接的なユニットテストという利点が得られると主張する人もいます。[ 18 ] [ 19 ]
テスト駆動開発(TDD)では、関連する本番コードを書く前に単体テストを作成します。まず失敗するテストを作成し、次にそのテストが合格するのに十分な本番コードを追加し、必要に応じてコードをリファクタリングし、最後に別の失敗するテストを追加してこのプロセスを繰り返します。
ユニットテストは、ユニットが設計どおりに動作し、意図どおりに機能することを保証することを目的としています。[ 20 ]
まずテスト可能な最小単位のテストを作成し、次にそれらの間の複合的な動作のテストを作成することで、複雑なアプリケーションに対する包括的なテストを構築できます。[ 20 ]
単体テストの目的の 1 つは、プログラムの各部分を分離し、個々の部分が正しいことを示すことです。[ 1 ]単体テストは、コードが満たさなければならない厳密な書面による契約を提供します。
単体テストは、開発サイクルの早い段階で問題を発見します。これには、プログラマの実装におけるバグと、ユニットの仕様の欠陥や欠落部分の両方が含まれます。徹底的なテストを作成するプロセスは、作成者に入力、出力、エラー条件について深く考えさせ、ユニットの望ましい動作をより明確に定義することを促します。[ 21 ]
コーディング開始前またはコードが最初に書かれた時点でバグを見つけるコストは、後でバグを検出、特定、修正するコストよりもかなり低い。リリースされたコードのバグは、ソフトウェアのエンドユーザーに高額な問題を引き起こす可能性もある。[ 22 ] [ 23 ] [ 24 ]コードの記述が不十分だと、単体テストが不可能または困難になる場合があるため、単体テストによって開発者は関数やオブジェクトをより適切に構造化せざるを得なくなる。
単体テストは、ソフトウェア開発におけるリリース頻度を高める。個々のコンポーネントを個別にテストすることで、開発者は問題を迅速に特定して対処でき、反復とリリースのサイクルを短縮できる。[ 25 ]
単体テストを行うことで、プログラマーは後日コードのリファクタリングやシステムライブラリのアップグレードを行った際に、モジュールが正しく動作することを確認できます(例えば、回帰テストなど)。手順としては、すべての関数とメソッドに対してテストケースを作成し、変更によって不具合が発生した場合に迅速に特定できるようにします。
単体テストは、設計契約に違反する可能性のある変更を検出します。
単体テストは、個々のユニットに関する不確実性を低減させることができ、ボトムアップ型のテスト手法で活用できます。プログラムの構成要素を最初にテストし、次にそれらを統合したテストを行うことで、統合テストがはるかに容易になります。
プログラマーの中には、単体テストはコードのドキュメントの一種であると主張する人もいます。ユニットが提供する機能やその使い方を学びたい開発者は、単体テストを確認することで理解を深めることができます。[ 26 ]
テストケースは、ユニットの成功に不可欠な特性を具体化することができます。これらの特性は、ユニットの適切な使用/不適切な使用、およびユニットによって捕捉されるべき負の動作を示すことができます。テストケースはこれらの重要な特性を文書化しますが、多くのソフトウェア開発環境では、開発中の製品の文書化にコードのみに依存しているわけではありません。
一部のプロセスでは、テストとテスト対象コードの記述、および関連するリファクタリングが形式的な設計の代わりとなる場合があります。各単体テストは、クラス、メソッド、および観測可能な動作を指定する設計要素と見なすことができます。[ 27 ]
テストでは、最も単純なプログラム以外ではすべての実行パスを評価できないため、プログラム内のすべてのエラーを検出することはできません。この問題は、決定不能な停止問題の上位概念です。ユニットテストについても同様です。さらに、ユニットテストは定義上、ユニット自体の機能のみをテストします。したがって、統合エラーや、より広範なシステムレベルのエラー(複数のユニットにまたがる機能や、パフォーマンスなどの非機能テスト領域など)を検出することはできません。ユニットテストは、特定のエラーの有無を示すことしかできず、エラーが全くないことを証明することはできないため、他のソフトウェアテスト活動と併せて実施する必要があります。すべての実行パスとすべての可能な入力に対して正しい動作を保証し、エラーがないことを確実にするには、他の手法、すなわち、ソフトウェアコンポーネントに予期しない動作がないことを証明するための形式手法の適用が必要です。
複雑なユニットテストの階層構造は、統合テストとは異なります。周辺機器との統合は統合テストに含めるべきですが、ユニットテストには含めるべきではありません。統合テストは通常、依然として人間の手によるテストに大きく依存しています。高レベルまたはグローバルな範囲のテストは自動化が難しいため、手動テストの方が速く、安価に見えることがよくあります。
ソフトウェアテストは組み合わせ問題です。たとえば、すべてのブール決定文には、少なくとも2つのテストが必要です。1つは結果が「true」になるテスト、もう1つは結果が「false」になるテストです。その結果、プログラマーは、記述したコード1行につき、3~5行のテストコードが必要になることがよくあります。これは明らかに時間がかかり、その労力に見合うだけの価値があるとは限りません。非決定性の問題や複数のスレッドが関わる問題など、簡単にテストできない問題もあります。さらに、単体テストのコードは、テスト対象のコードと同様にバグがある可能性が高いです。フレッド・ブルックスは著書『人月の神話』の中で、「クロノメーターを2つ持って海に出てはいけない。1つか3つ持っていくべきだ」と述べています。[ 28 ] (2つのクロノメーターが矛盾する場合、どちらが正しいかはわかりません。)
単体テストの作成に関連するもう1つの課題は、現実的で有用なテストを設定することの難しさです。テスト対象のアプリケーション部分がシステム全体の一部のように動作するように、適切な初期条件を作成する必要があります。これらの初期条件が正しく設定されていない場合、テストは現実的なコンテキストでコードを実行することにならないため、単体テストの結果の価値と精度が低下します。[ 29 ]
単体テストから期待される効果を得るためには、ソフトウェア開発プロセス全体を通して厳格な規律が求められる。
実施したテストだけでなく、このソフトウェアユニットまたは他のユニットのソースコードに加えられたすべての変更についても、注意深く記録しておくことが不可欠です。バージョン管理システムの使用は必須です。ユニットの最新バージョンが、以前は合格していた特定のテストに不合格になった場合、バージョン管理ソフトウェアは、それ以降にユニットに適用されたソースコードの変更(もしあれば)の一覧を提供できます。
テストケースの失敗が定期的にレビューされ、即座に対処されるようにするための持続可能なプロセスを実装することも不可欠です。[ 30 ]このようなプロセスが実装されず、チームのワークフローに定着しない場合、アプリケーションは単体テストスイートと同期せずに進化し、誤検出が増加し、テストスイートの有効性が低下します。
組み込みシステムソフトウェアの単体テストは、ソフトウェアが最終的に実行されるプラットフォームとは異なるプラットフォームで開発されるため、デスクトッププログラムのように実際の展開環境でテストプログラムを実行することが難しく、課題があります。[ 31 ]
メソッドに入力パラメータと出力がある場合、単体テストは最も簡単に作成できます。メソッドの主要な機能がアプリケーション外部の何かとやり取りすることである場合、単体テストを作成するのはそれほど簡単ではありません。たとえば、データベースを操作するメソッドでは、データベースとのやり取りのモックアップを作成する必要があるかもしれませんが、それは実際のデータベースとのやり取りほど包括的ではないでしょう。[ 32 ]
以下はJUnitテストスイートの例です。このテストスイートはAdderクラスに焦点を当てています。
class Adder { public int add ( int a , int b ) { return a + b ; } }このテストスイートは、assert文を使用して、メソッドへのさまざまな入力値に対する期待される結果を検証しますsum。
import static org.junit.Assert.assertEquals ; import org.junit.Test ; public class AdderUnitTest { @Test public void sumReturnsZeroForZeroInput ( ) { Adder adder = new Adder (); assertEquals ( 0 , adder.add ( 0 , 0 ) ); }@Test public void sumReturnsSumOfTwoPositiveNumbers () { Adder adder = new Adder (); assertEquals ( 3 , adder . add ( 1 , 2 )); }@Test public void sumReturnsSumOfTwoNegativeNumbers () { Adder adder = new Adder (); assertEquals ( - 3 , adder . add ( - 1 , - 2 )); }@Test public void sumReturnsSumOfLargeNumbers () { Adder adder = new Adder (); assertEquals ( 2222 , adder . add ( 1234 , 988 )); } }単体テストを設計仕様として使用することには、他の設計手法に比べて大きな利点が1つあります。それは、設計ドキュメント(単体テストそのもの)を実装検証に利用できる点です。開発者が設計通りにソリューションを実装しない限り、テストは決して合格しません。
単体テストは、UML図のような図式仕様ほどのアクセシビリティに欠ける部分があるが、自動化ツールを使用して単体テストから生成することができる。ほとんどの最新の言語には無料のツールがある(通常はIDEの拡張機能として利用可能)。xUnitフレームワークに基づくツールのような無料ツールは、人間が理解するためのビューのグラフィカルなレンダリングを別のシステムに委託する。[ 33 ]
ユニットテストはエクストリームプログラミングの基盤であり、自動化されたユニットテストフレームワークに依存しています。この自動化されたユニットテストフレームワークは、 xUnitなどのサードパーティ製のもの、または開発グループ内で作成されたもののいずれかです。
エクストリームプログラミングでは、テスト駆動開発のために単体テストを作成します。開発者は、ソフトウェアの要件または欠陥を明らかにする単体テストを作成します。このテストは、要件がまだ実装されていないか、既存のコードの欠陥を意図的に明らかにするため、失敗します。次に、開発者は、このテストと他のテストがすべて合格するように、最もシンプルなコードを作成します。
システム内のほとんどのコードは単体テストされていますが、必ずしもコード内のすべてのパスがテストされているわけではありません。エクストリームプログラミングでは、従来の「すべての実行パスをテストする」方法ではなく、「壊れる可能性のあるものすべてをテストする」戦略が必須となっています。これにより、開発者は従来の方法よりもテストの数が少なくなりますが、これは実際には問題ではなく、事実の再確認です。従来の方法では、すべての実行パスが徹底的にテストされるほど体系的に実行されたことはほとんどなかったからです。[ 34 ]エクストリームプログラミングは、テストが網羅的になることはめったにない(経済的に実行可能になるにはコストと時間がかかりすぎることが多いため)ことを認識し、限られたリソースを効果的に集中させる方法についてのガイダンスを提供します。
重要な点として、テストコードは実装コードと同じ品質で維持され、重複がすべて排除されるため、第一級のプロジェクト成果物とみなされます。開発者は、テスト対象のコードと併せて、単体テストコードをコードリポジトリにリリースします。エクストリームプログラミングの徹底した単体テストにより、よりシンプルで確実なコード開発とリファクタリング、簡素化されたコード統合、正確なドキュメント作成、よりモジュール化された設計など、前述の利点が得られます。これらの単体テストは、回帰テストの一種として継続的に実行されます。
単体テストは、単体テストの作成、テストの合格、リファクタリングという短いテスト駆動開発サイクルを通じてソフトウェア設計を反復的に開発するEmergent Designの概念にとっても重要です。単体テストは、継続的なリファクタリングを安全にする回帰セーフティネットを提供します。[ 35 ]
自動テストフレームワークは、テスト実行を自動化する機能を提供し、テストの作成と実行を迅速化できます。フレームワークは、さまざまなプログラミング言語向けに開発されています。
一般的に、フレームワークはサードパーティ製であり、コンパイラや統合開発環境(IDE)とは別に配布されます。
テストは、フレームワークを使用せずに、アサーション、例外処理、その他の制御フローメカニズムを使用してテスト対象のコードを実行し、動作を検証して失敗を報告することで作成できます。フレームワークの採用には参入障壁があるため、フレームワークを使用せずにテストを行うことは価値があると指摘する人もいます。また、テストがないよりはテストがある方が良いという意見もありますが、フレームワークが導入されると、テストを追加するのは容易になります。[ 36 ]
一部のフレームワークでは、高度なテスト機能が欠けており、手動でコーディングする必要がある。
一部のプログラミング言語は、ユニットテストを直接サポートしています。これらの言語の文法では、ライブラリ(サードパーティ製か標準ライブラリかを問わず)をインポートすることなく、ユニットテストを直接宣言できます。さらに、ユニットテストのブール条件は、非ユニットテストコードで使用されるブール式と同じ構文で表現できます。例えば、` ifand`whileステートメントで使用されるものと同じです。
ユニットテストを組み込んだプログラミング言語には、以下のようなものがあります。
標準的な単体テストフレームワークをサポートする言語には、以下のものがあります。
一部のプログラミング言語には組み込みの単体テスト機能はないものの、確立された単体テストライブラリやフレームワークが存在します。これらの言語には以下が含まれます。
{{cite book}}ISBN /日付の不一致(ヘルプ)請負業者は、ソフトウェアユニットをコーディングおよびテストし、テストに成功した各ユニットのソースコードとオブジェクトコード、および関連するリストを開発構成に入力するものとする。
{{cite journal}}: CS1メンテナンス: DOIは2026年5月現在非アクティブです(リンク)