United Front 2 ではシステム全体を通して 契約による設計 の概念を取り入れ、実装 しています。契約による設計はシステムの信頼性を高める手法のひとつで、クラス間の関係を互いの権利と義務を表す 正式な同意であると見なし、クラス同士が結ぶ契約を 表明 という形で定義しています。ここ では、United Front 2 が契約による設計をどのように実現しているのかを述べています。契約による設計自体の 解説は、提唱者(バートランド・メイヤー)の書籍 オブジェクト指向入門 を参照してくださ い。
United Front 2 では、表明を通常の仕様(JavaDoc)に記述し、その検証を通常の JUnit 単体テストで実装 することで、契約による設計を実現しています。これはあまり工夫点がないように思えるかもしれませんが、一般的な手 法から離れることなく契約による設計を取込めるメリットがあります。本章ではその仕様の書き方と単体テストの実装指 針を示しています。
契約による設計の実現を支援するソリューションとしては、Java 言語で用意されている assert
文を使う方法の他、Contract4J
、
ESC/Java2
、
SpringContracts
などの製品を利用する方法があります。assert 文は Java 言語における本来の表明ですが、
一般的でなく、単体テストなどと連携しにくいというデメリットがあります。また各製品は魅力的ですが、採用した場合、
仕様記述が製品固有の厳密なルールによって強制されることになります。
契約による設計では、ソフトウェアの仕様を明記するのに 表明 が用いられます。表明を正しく 記述することで、厳密かつシンプルな仕様を書くことができます。United Front 2 では、表明の記述を JavaDoc に記述しています。その際、記述を簡単にするために、次のようなカスタムタグを利用できます。
| JavaDoc タグ | 対応する表明 | 記述できる場所 |
|---|---|---|
@require |
事前条件 | メソッド |
@ensure |
事後条件 | メソッド |
@invariant |
クラス不変表明 | クラス、プロパティ |
表明の JavaDoc タグはひとつの仕様の中に複数記述することができます。このとき、同種の表明は
and で結ばれます。例えば次の記述は、事前条件として「Aが成り立ち、かつBが成り立つ」という
意味になります。
@require Aが成り立つ @require Bが成り立つ
表明の記述には変数を用いることができます。変数は他の記述と区別するために、EL 式のように
${} でくくって表現します。EL 式と異なる点は、変数のメソッドを呼び出せる点です。例えば次の
記述では、変数 arg0 、arg1 およびメソッド identify
を使っています。
@ensure ${arg0.id} is not null.
@ensure ${arg0.identify(arg1)} is true.
また、表明の中で次のような特殊変数および関数を利用できます。
| 変数/関数 | 説明 |
|---|---|
this |
自分自身を参照する変数です。事前条件に記述されている場合、メソッド実行前の自身のオブジェクトを、事後条件に記 述されている場合メソッド実行後の自身のオブジェクトを、クラス不変表明に記述されている場合不変的な自身のオブ ジェクトを参照します。 |
return |
返り値を参照する変数です。 |
old(..) |
引数で渡した変数の、メソッド実行前の状態を表します。 |
事前条件および事後条件はメソッドに対して記述します。記述例として、データアクセスインターフェース
SampleDomainDao の update メソッドの仕様を示します。
/**
* ${sampleDomain} を更新します。
*
* @param sampleDomain サンプルドメイン
* @require ${sampleDomain.id} != null.
* @require ${this.find(sampleDomain.id)} != null.
* @ensure ${old(sampleDomain).id} equals ${sampleDomain.id}
* @ensure ${sampleDomain} equals ${this.find(sampleDomain.id)}
*/
void update(SampleDomain sampleDomain);
このコード例は、次のように文書によって記述しても同様の意味となります。
/** * サンプルドメインオブジェクトを更新します。 * * @param sampleDomain サンプルドメイン * @require 引数として渡すサンプルドメインの ID が null でない * @require 引数として渡すサンプルドメインが存在する * @ensure 引数として渡すサンプルドメインの ID は、メソッドの実行前と実行後で変化がない * @ensure 引数として渡すサンプルドメインがデータベースと同期する */ void update(SampleDomain sampleDomain);
前者はより数式に近い形で、後者は文書の形で仕様を定義しています。United Front 2 では、どちらの形式で記 述しても良いことにしています。
前者の記述は後者に比べ、より具体的に仕様を定義しています。特に最後の事後条件は、
SampleDomainDao が持つ find メソッドをうまく使って具体的に定義して
おり、前者の記述がより優れています。ただし、どんなときも数式表現が優れているかというとそういうわけではありま
せん。なぜなら、仕様は「わかりやすいこと」も重要な要件だからです。いくら具体的に記述したところで、難解な仕様は
理解されません。そうしたケースでは、理解しやすい文書表現を選択する方が有利となることもあります。
メソッドをうまく使うことで、仕様の記述をシンプルにすることができます。上記の例では、update
メソッドの表明の中に find メソッドをうまく利用しています。既存メソッドの利用は、特に仕様がよ
り複雑になる高レベルメソッドの仕様記述において有効になります。
クラス不変表明はクラス全体に対して記述する表明で、主にドメインクラスが対象となります。各プロパティやその制約を
明記します。記述例として、単純なドメインモデルを示します。この例では、code プロパティの表明
をプロパティドキュメントに、プロパティ間に渡る表明をクラスドキュメントに記述しています。
/**
* サンプルドメインのドメインモデルです。
*
* @invariant ${this.overview.readAccessControl} equals ${this.readAccessControl}
*/
public class SampleDomain ... {
/**
* コード
*
* @invariant 一意な値
*/
private String code;
/** 概要 */
private Message overview;
/** 参照権限 */
private AccessControl readAccessControl;
// ...
}
上述のメソッド仕様を JUnit 単体テストを使って検証します。以下の例では、契約による設計によって作られた仕様を そのまま JUnit テストコードとして実装しています。
@Autowired private SampleDomainDao sampleDomainDao;
@Test
public void testUpdate() {
// 初期処理
SampleDomain sampleDomain = new SampleDomain();
// ...
sampleDomainDao.register(sampleDomain);
// ...
// 事前条件の確認
Assert.assertNotNull(sampleDomain.getId());
Assert.assertNotNull(sampleDomainDao.find(sampleDomain.getId()));
// メソッド実行
Integer oldId = sampleDomain.getId();
sampleDomainDao.update(sampleDomain);
// 事後条件の確認
Assert.assertEquals(oldId, sampleDomain.getId());
Assert.assertEquals(sampleDomainDao.find(sampleDomain.getId()), sampleDomain);
}
United Front 2 では、仕様は純粋な文書であり、仕様をテストデータを使ってそのまま実装したものがテストケー スになります。この考え方により、仕様を書いた時点でテストコードを実装できるようになるため、United Front 2 では テストファースト によって開発を進めることができます。テストケースは機能が追加され るたび、不具合が発見されるたびに追加する必要があります。
どのメソッドまでを単体テストの対象とすべきでしょうか?getter や setter といった単純なメソッドをテストすること にほとんど価値がないのは明白です。その他、処理の全てを他に委譲しているようなメソッドや、条件分岐やループが 全くないメソッドもテストする意味がほとんどない場合があります。明確な判断基準は特に設けていませんが、不要と考 えられるテストケースは実装しない方が得策です。
ここでのテストは仕様に基づいて行われるテストですので、ブラックボックステスト と言えま す。本来テスト - 厳密には機能的なテスト - は、ソフトウェアが仕様通りに動くことを確認するためのものですから、ブ ラックボックステストが中心となるべきです。それに対し、実装に基づいて行われるのがホワイトボックステストですが、 契約によるの設計の概念を考えればその役割は希薄です。 EasyMock や jMock などを使ったモックオブジェクトによるテスト、 DbUnit などを使ったデータベース実装依存の単体テストは、 ホワイトボックステストに該当することが多いようです。テストフレームワークもテストの目的に応じて選択する必要があ ります。
メソッドを使って記述された仕様のテストには、必ず依存関係が存在します。例えば上記の例では、
update メソッドのテストは find メソッドが正しく動作していることを前提とし
ています。find のようなメソッドは多くの仕様記述で利用されるため、find
メソッドが正しければ連鎖的に他のテスト結果も正しいと言うことができます。それでは、find
メソッドの正しさはどのように証明すればよいのでしょうか?現実的な話、find メソッドのテストす
ら、register などのメソッドを使わずに実装することは困難です。ということは、そもそも本当に表
明の中にメソッドを用いて良いのか疑問に思われるかもしれません。
この問題は、仕様をテストによって証明することはできない という事実を物語っています。テ ストのほとんどは、結局のところ、無限に存在し得るテストケース中の高々数ケースを取り挙げているに過ぎないからで す。仕様を他のメソッドを使わずに記述したところで、そのメソッドの正しさを 証明 することな ど不可能なのです(証明できなくても、正しく動くメソッドは実装できます。テストによって正しいことをほぼ確信できます)。 独立した仕様を記述しようとすると、現実的な問題に対する有効性が低下します。そうであれば、仕様をよりシンプルに する、メソッドを使った記述法を使わない手はありません。これらについての議論は、オブジェクト指向入門 の 11.14.3 章でも述べられています。