オブジェクト指向による「テスト」の分類、その前提にある「概念水準」・「仕様水準」・「実装水準」の差異に関して

オブジェクト指向の考え方を利用すると、開発プロセスの一環として実施すべき「テスト」が適切に整理できるようになる。また、「テスト」という概念を適切に整理すると、逆にオブジェクト指向の考え方も先鋭化させることが可能になる。

要約すれば、私はオブジェクト指向分析で言及すべき「概念水準の観点(conceptual perspectives)」、オブジェクト指向設計で言及すべき「仕様水準の観点(specification perspectives)」、オブジェクト指向プログラミングで言及すべき「実装水準の観点(implementation perspectives)」の差異に対応付けて、各テストを「機能テスト」、「結合テスト」、「単体テスト」に区別している。

この概要の一文だけでも、少なくても三種類の区別が導入されている。

  1. オブジェクト指向分析とオブジェクト指向設計とオブジェクト指向プログラミングの差異。
  2. 概念水準(conceptual level)と仕様水準(specification level)と実装水準(implementation level)の差異。
  3. 機能テストと結合テストと単体テストの差異。

以下では、これらの区別について順に説明していく。

分析・設計・プログラミングの差異と概念・仕様・実装の差異

責任としてのオブジェクト

オブジェクト指向におけるオブジェクトは単なる「データと操作の集合体」ではない。この前提に立ってしまうと、上述した三つの区別を理解することはできない。むしろオブジェクトは「責任」を意味する。インスタンスは責務を遂行する実体に他ならない。

概念水準のオブジェクト指向分析

RUP:Rational Unified Processのようにオブジェクト指向に準拠した開発プロセスでは、まず全てソフトウェアを網羅的に設計するのではなく、コアとなるアーキテクチャや基本的な仕様となるサブセットを設計していく

そのためには、まず開発しようとしているオブジェクトが「そもそも何なのか(What)」を特定しなければならない。初めからそのオブジェクトが「如何にして実装されるのか(How)」を考えてしまっては、何を造れば良いのかも定まらない状況で開発を進めることになるため、不確実性が高まってしまう。

この「そもそも何なのか(What)」、すなわち「何の責任を担うのか」を特定する営みを概念水準での分析と呼ぶ。この分析の具体名がオブジェクト指向分析であると考えて良い。

実装水準のオブジェクト指向プログラミング

一方、やはり最終的には「如何にして実装されるのか(How)」も決定しなければならない。この営みが実装水準となる。そして、この実装の方法がオブジェクト指向プログラミングということになる。

しかし、一つの開発で要求される責任が単一とは限らない。複数の責任が問題となる場合、複数のオブジェクトが必要になる。場合によっては、オブジェクト同士を結合することで、複合的な責務を担保しなければならなくなる。

仕様水準のオブジェクト指向設計

そこでオブジェクト指向では特に、複合的な責任を果たそうとしている複数のオブジェクトの結合部分となる公開インターフェイスの仕様化が目指されることになる。ここでいう公開インターフェイスの仕様化とは、あるオブジェクトがそのオブジェクトを「如何にして利用できるのか(How)」を共有する営みを意味する。

いわゆる「仕様」とは、インターフェイスのHow to use it.に他ならない。使い方がわからなければ、仕様書など無いも同然だ。そして、この仕様を共有する営みこそがオブジェクト指向設計に他ならない。GoFのデザイン・パタンは、まさに共通言語化による共有を目指した産物だった。

概念水準の分析結果と仕様水準のインターフェイスを介したコミュニケーションのメリット

概念水準や仕様水準で要求や仕様の共有を行なえば、システムの概要を伝達することができる。実装水準のコードの詳細を知らなくてもコミュニケートすることが可能になる。例えばRUP:Rational Unified Processソフトウェア要求定義アーキテクチャ設計は、まずこの概念水準と仕様水準でのみシステムを分析して設計していく。

この区別を導入していると、概念や仕様を変更せずに実装を変更することができるため、コードが改修されたとしても、その実装の詳細を知らない者でも、インターフェイスを介してそのオブジェクトを利用し続けることができる。これは、インターフェイスのユーザを実装の変更による影響から守ることを意味する。

REST(ful)の仕様は、この概念と仕様の水準でコミュニケートしたわかり易い事例になっている。各APIのユーザは、連携先のシステム内部の具体的な実装内容を全く知らない状態であっても、仕様として規定したHTTP(S)リクエストとレスポンスの形式に従うだけで、そのAPIを運用することができる。

機能テストと結合テストと単体テストの差異

機能テストと結合テストと単体テストの差異は、責任という概念を前提として、概念水準と仕様水準と実装水準の差異に対応している。次のように纏めても良い。

  • 機能テストでは、成果物がオブジェクト指向分析結果と対応しているか否かを検証する。
  • 結合テストでは、成果物がオブジェクト指向設計結果と対応しているか否かを検証する。
  • 単体テストでは、オブジェクト指向プログラミング結果となるコードが成果物と見做せるか否かを検証する。

以下、もう少し詳述する。

機能テストの前提

機能テストは概念水準の検証になる。「そもそも何なのか(What)」、すなわち「何の責任を担うのか」に関する当初の予定と、実際に実装されたコードをはじめとした成果物との間に矛盾が無いことを確認していく。

RUP:Rational Unified Processの場合は特に、ソフトウェア要求定義などで取り上げたユースケース図やユースケース記述に成果物たるオブジェクトが対応し切れるか否かを検証する。いわゆる「ユースケース・テスト」は、ユースケース・モデルとコードの同期関係を確認するテストに他ならない。オブジェクト指向分析で組み立てたモデルと実際のコードが同期していなければ、そもそもその成果物は何の責任も果たしていないことになる。無価値と見做されても致し方ない。

ユースケース・テスト」に象徴されるように、一連の機能テストでは、システムの内部を事実上考慮しない。丁度、ユースケース図がアーキテクチャの内部を事実上考慮しない理屈と同じだ。その意味で機能テストは「ブラックボックステスト」であり、「限界値分析」や「同値分割」は「ユースケース・テスト」の機能的等価物として採用されても構わない。

結合テストの前提

結合テストは仕様水準の検証になる。仕様によって共有されるのは、そのオブジェクトが「如何にして利用できるのか(How)」だ。したがって結合テストで検証すべきなのは、そのオブジェクトのインターフェイスが本当に仕様で規定されている通りに利用できるか否かになる。

言い換えれば、結合テストでは、インターフェイス同士の結合を検証する。これにより、各オブジェクトのインターフェイスを介した相互利用が「仕様通り」に実現することを確認する。オブジェクト指向設計で組み立てたインターフェイスに関するモデルと実際のコードが同期していなければ、使用で説明しているHow to use it.に矛盾があるということになる。

その影響は他のオブジェクトに波及している。もし矛盾が見受けられるのならば、そのインターフェイスを抱えているオブジェクトの改修が必要になる。これは一種の「手戻り」であり、別途で退行テストが必要になる。ここでいう退行テストは、概ね単体テストのやり直しと考えて良い。

結合テストケースを考察する上では、多くの場合「粒度」が問題になる。同じ結合部分でも、「APIとAPIのマッシュアップ」と「PHPとMySQLの結合」とでは、粒度に差異が生じる。しかしこの問題に対する普遍的な正解は無い。どの粒度が適切なのかは、専らオブジェクト指向設計で選択する必要がある。

単体テストの前提

単体テストは実装水準の検証になる。つまり、「如何にして実装されるのか(How)」が問われる。詳細クラス図やシーケンス図やコミュニケーション図は実装を手引きしてくれるモデルとしても機能する。

しかしながら、実際のコードがこれらのモデルと同期していなければ、仕様を満たせないオブジェクトとして実装されている危険がある。また、モデルとコードが同期していないということは、後続の開発でそのモデルを見直しても、現状のコードを俯瞰的に観察することが不可能になる。これは、ソフトウェアの全体像を把握することが不可能であるということになるため、後続の開発者との仕様水準でのコミュニケーションが事実上不可能になっていることを意味する。

一般的に単体テストは致命的なバグを取り除くデバッグ作業と見做されている。いわゆる「ホワイトボックステスト」や「デシジョンテーブル」では、「分岐網羅」や「命令網羅」や「条件と動作の差異」などのような概念によって網羅性が問われている。

しかし、こうした網羅性の問題は二次的な問題として格下げしてしまって構わない。仮にあらゆる条件や可能性を考慮したテストを実施したところで、最終的に提出される成果物たるコードがモデルと同期していないようでは、利用価値が無い。後続の開発による保守や拡張において、モデルによる俯瞰的な全体像の観察ができないようでは、「再利用」の対象にはなり得ない。

むしろ単体テストの時点で消化しておくべきなのは、「クラス構造テスト」などのような静的テストだろう。個々のオブジェクトにバグが無い場合であっても、クラス間の継承、多重度、凝集度、可視性などによって、思わぬ修正や機能追加を余儀無くされる場合もある。

オブジェクト指向設計思想に関する参考文献

コメントをどうぞ