ユニットテストの基礎
1. ユニットテストとは
ユニットテスト(単体テスト)は広い意味で使われるようになっている言葉です。
まず、伝統的には、ユニットテストはテストレベルのひとつであり、「個々のユニットを対象とするテスト」としばしば定義されます(例えばJSTQB用語集[JSTQB-glossary.V2.0.J02])。なお、ここでいうユニットとはソフトウェアの最小構成単位を指します。例えば、ソースコードが対象であればC言語等では関数が、JavaやC++等ではクラスが一般的にユニットに該当します。詳しくは後述しますが、そうした関数やクラスを一つ一つ検証していくのが伝統的なユニットテストです。
一方、他の定義として、開発者テストやアジャイル開発の分野では、ユニットテストという言葉が「ソースコードレベルのインターフェースを使ってソースコードを検証するテスト」という、広い意味で使われることがあります。「ソースコードレベルのインターフェースを使って」というのは、例えば、ソースコード上のメソッドの引数に入力値をセットし、その戻り値を検証するようなやり方です。こちらの定義では、一度に複数のユニットをまとめて検証するテストや、外部ライブラリとユニットをセットにして検証するテストも、ユニットテストと呼称することがあります。
2. ユニットテストの形式
ユニットテストは大きく静的テストと動的テストの2種類に大別されます。
まず静的テストは、ソースコードを実行させずにテストを行うアプローチで、手段としてコードレビューやソースコードの静的解析が該当します。例えば以下は静的なユニットテストに該当します。
- 個人や組織でソースコードをレビューし、設計仕様書と矛盾はないか、バグは無いかなどをチェックする
- ソースコードの静的解析ツールを使用して、コーディング規約の違反はないか、リスクやバグはないか、メトリクスは妥当か、といった診断を行う
一方動的テストは、一般的にユニットテスティングフレームワークを使用して、テスト対象であるユニットを動かし、その動作結果を検証するアプローチを取ります。 例として動的なユニットテストの実例を以下に示します。これは関数 (isLeapYear)を検証するC++言語のユニットテストです。
TEST(isLeapYearTest, 400で割り切れる年はうるう年と判定する) { EXPECT_EQ(true, isLeapYear(800)); EXPECT_EQ(true, isLeapYear(400)); }
上記では、テスト対象 (isLeapYear)に800、400という引数を指定し、実際にそれを実行させて、戻り値を期待値trueと比較して検証する、という処理を行っています。
3. ユニットテストの目的
ユニットテストはその手軽さから様々な目的で利用されます。特に大きな用途は、Vモデルにおけるユニットテストと、開発者テストとしてのユニットテストです。
3.1. Vモデルにおけるユニットテスト
Vモデルでは、ユニットがユニット仕様(詳細仕様)を満たしていること、またユニットが次の工程に移行してもよい水準の品質を実現していることを検証するための手段として、ユニットテストを運用します。この目的のユニットテストは、しばしば実装工程後に実行されます。
3.2. 開発者テストとしてユニットテスト
一方、開発者テストとしてのユニットテストでは、便利なプログラミング支援ツールとしてさまざまな目的でユニットテストを活用します。主要な活用例を以下にまとめます。
回帰テストとしてのリファクタリングの支援
リファクタリングにおいて、挙動が変化していないか、バグが混入していないかチェックする回帰テストとして、ユニットテストがしばしば活用されます。
手順としてはリファクタリング前に対象のユニットテストを十分に記述し、ユニットテストが失敗しないか監視しながらリファクタリングを進めていくスタイルが一般的です。
継続的インテグレーションでのバグ混入の監視
継続的インテグレーションでは、実施タスクの一部にユニットテストがしばしば組み込まれます。そのユニットテストは、ソースコードにバグが混入しないか継続的に監視する監視役として活用されます。
テストファーストによる設計支援
テスト駆動開発といったテストファースト手法では、設計の検討・改善の支援や、プログラミングの進捗確認の手段として、ユニットテストを頻繁に活用します。この目的に関しては、テスト駆動開発(後日公開)の項を参照下さい。
仕様書としての活用
開発者テストとしてユニットテストを活用する現場では、ユニットテストのテストコードを、ソースコードのふるまいを説明するドキュメントとして扱う場合があります。特にドキュメントとしても扱えるように可読性や構造を工夫したユニットテストは「仕様化テスト(characterization test)」と呼ばれます。仕様化テストのC言語での例を以下に示します。これはうるう年判定関数isLeapYear()のユニットテストです。自然言語のようにテストコードを記述するため、CSpecというBDDフレームワークを使用しています。
DESCRIBE(isLeapYear, "isLeapYear") IT("returns 1 if input year is divisible by 4") SHOULD_EQUAL(DESCRIBE(4), 1) END_IT IT("returns 1 if input year is divisible by 400") SHOULD_EQUAL(hoge(800), 1) END_IT END_DESCRIBE
4. ユニットテストの効果と課題
ユニットテストではテスト対象をユニット単位で選択できる点、ソースコードのレベルでテスト対象を操作できる点から、以下の様なメリットがあります。
- 全体が完成していなくても、テスト対象のユニットさえ確保できればテストが実施可能です。例えばプログラミング直後から実施可能であり、バグの早期検出を実現できます。
- 開発環境上でテストを実施することができ、実行環境の制約を緩和できます。例えば組み込みのクロス開発では、ターゲット環境は実行速度が遅い、環境が開発後半まで手に入らない、ターゲット環境へのロードや結果の出力に手間がかかる、といった制約を受けがちです。ユニットテストではそうした制約を回避しながらテストを実施することができます。
- UIといった外部のインターフェースから分離して動的テストを実行できるため、結合テスト、システムテストと比べ動的テストの自動化が容易です。また実行速度も一般的に高速です。
- ソースコードレベルでテストの入力を操作したり検証したりすることができるため、通常操作では再現の難しい、特殊な例外処理や組み合わせもテストできます。
ただ上記のようなメリットを持つ一方、以下のような課題も持ち合わせています。
- 動的なユニットテストの実装はプログラミングそのものなので、実装や管理にはプログラミングスキルが要求されます。保守性を確保するためには、綺麗な設計やスタイルが求められるほか、バージョン管理といった適切な構成管理も要求されます。
- ユニットテストでは、ユーザインターフェースや機能といったシステムレベルのテスト対象を検証するのが困難です。ユニットテストを網羅的に実施したといっても、システムとしてバグがないと保障できません。
- 動的テストの場合、テスト対象のインターフェースとテストが強く結合します。ソースコードを変更すると、それに合わせてテストもたくさん変更しなければならないことがあります。
- 動的テストの場合、テスト対象が依存するコンポーネントを切り離すために、テストダブル(テストスタブやモック、フェイクオブジェクト等の総称)を組み込む手間が発生することがあります。
- テスト対象に高いテスタビリティを要求します。複雑に結合したユニットに対してユニットテストを作成するのはかなり困難です。
ユニットテストは、このように効果と課題の両面を併せ持ちます。特に、
「ユニットテストは回帰テストやテスト駆動開発を実現し変更を容易にする」
「ユニットテストはテスト対象と結合し、テスト対象の変更を難しくする」
というように、相反する二面性を同時に抱えている点に留意が必要です。ユニットテストのデメリットを抑えつつ、メリットを活かすためには、ユニットテストをいつ、どのような目的で、どのような段取りで活用するかという、テスト戦略を工夫する必要があります。
5. ユニットテストの設計と実装
5.1 ユニットテストの設計
ユニットテストをどのように作っていくかですが、まずユニットテストの設計においては、ホワイトボックスのテスト設計技法、ブラックボックスのテスト設計技法両方を活用していくことになります。
例えば詳細仕様として関数のふるまいがインプットされれば、それに対し同値分割や境界値分析等を適用して、ユニットテストを設計していきます。また詳細仕様としてフローチャートがインプットされれば、フローチャートに対するカバレッジ分析等を通して、テストケースを作っていきます。
なおユニットテストの網羅性については、指標の一つとしてコードカバレッジがしばしば使用されます。コードカバレッジは、ユニットテストがテスト対象のソースコードをどの程度網羅しているかの指標であり、テスト漏れの検出等で有効に使用できます。ただ以下の2点には注意が必要です。
- コードカバレッジが高いといってもテストの網羅性が十分とは言えません。例えばコードカバレッジ100%といっても、コードの欠落や仕様との矛盾は検出できません。
- 100%といった高水準のコードカバレッジの実現は、例外処理や外部依存の処理といった障害が珍しくないことから、しばしば非効率な作業となります。
こうした事情から、ユニットテストの網羅性の適切な評価には、コードカバレッジ以外にも、仕様に対する網羅性やコードレビューでのチェックといった指標を組み合わせ、複合的に判断するアプローチが不可欠です。
5.2 ユニットテストの実装
次にユニットテストの実装についてですが、一般的にテスト対象と同じプログラミング言語で記述されます。例えばC#でテスト対象の実装を記述しているならば、一般的にユニットテストもC#で実装されます。
なおユニットテストの実装はプログラミングそのものですから、主に開発者が開発中やユニットテスト工程に記述することになります。特に継続的インテグレーションにおいてユニットテストを活用する場合は、「プログラマが責任を持って自分のコードにテストを書く」「ソースコードをコミットする際はそれを検証するユニットテストも一緒にコミットする」といった、プログラマ自身にプログラミングと並行してユニットテストを作成させる慣習が推奨されることが少なくありません。
6. ユニットテストを支えるツール
ユニットテストはさまざまな目的で活用されていることもあり、関連ツールも多様です。一般的なツールを以下に示します。
ユニットテスティングフレームワーク
動的なユニットテストの活動全体を支えるフレームワークです。ユニットテストの事前条件のセット、テストの実行、テスト結果の通知、ユニットテスト実装に役立つAPIや機能の提供、といった役割を担います。代表的なツールとしてはJUnit、RSpec、NUnitなどがあります。JUnitについては別項(後日公開)で詳しく解説します。
静的解析ツール
ソースコードの品質を検証するツールです。コーディング規約との適合性、メトリクスの評価、品質モデルに基づいた評価、バグやリスクのある記述の抽出、といったさまざまな用途のツールが存在します。Check Style、FindBugsなどが著名です。静的なユニットテストで一般的に使用されます。
コードカバレッジ計測ツール
動的なユニットテストでのコードカバレッジを計測するツールです。このツールは、ユニットテストがきちんとテスト対象を網羅しているか、仕様書とソースコードが一致しているか、といった評価に使用されます。
7. ユニットテストをはじめるには
ユニットテストでは、フレームワークの構築、ホワイトボックスによるテスト設計、テストコードのプログラミング、テストの管理、テスタビリティの作り込みといった、開発者寄りのスキルが多方面に要求されます。それらスキルの習熟度はユニットテストの導入効果に直結しますので、ユニットテストを導入する際はプログラミングの改善活動を行う心意気で技術蓄積を行うと良いでしょう。
ユニットテストはメリット・デメリット両方を併せ持つものですが、十分にスキルを積めば、高品質なソースコードを効率的に開発できるようになる、強力な開発支援ツールとなります。
参考文献:
ソフトウェアテスト標準用語集(日本語版)Version 2.0.J02 (2011年04月19日)
International Software Testing Qualifications Board 用語集作業班
執筆者: goyoki プロフィール
WACATE実行委員 / TDD研究会 / TDDBC
医療機器の開発やテストに従事。テストと開発のコラボレーションやテストの効率化について検討を行なっているほか、テストに関して講演や執筆活動を続けている。