ソフトウェアのテストでは、有用で包括的なテスト・データの作成は重要なステップの 1 つです。これを達成するための方法として、データ区分 技法 (ドメイン分析とも呼ばれる) の使用が有益であることが分かっています。
データを区分化すると、有用なテスト値を選択し、最善のテスト範囲を達成するという作業を効率よく行えます。このトピックでは、データ区分化技法をオブジェクト指向のシステム (具体的には、複合オブジェクト型パラメーターと外部入力またはコンテキスト・ベースの入力を含むシステム) に適用する場合について、これらの技法の使用法を重点的に説明します。
一般的には、次のようにしてテスト・データを区分化します。
このトピックで使用する各例は、JUnit の Money アプリケーションから引いた例で、特に Money クラスの equals メソッドに焦点を当てています。このメソッドのコードは、以下のコード・リストに表示されています。
public class Money implements IMoney { private int fAmount private String fCurrency; public boolean equals(Object anObject) { if (isZero()) if (anObject instanceof IMoney) return ((IMoney)anObject).isZero(); if (anObject instanceof Money) { Money aMoney= (Money)anObject; return aMoney.currency().equals(currency()) && amount() == aMoney.amount(); } return false; }
テスト・データの定義の最初のステップは、テスト対象コンポーネント (CUT: component-under-test) がどのデータを使用するかを理解することです。 equals メソッドを例として使用すると、このメソッドはオブジェクト型の引数 (anObject) を 1 つ使用することが分かります。
public boolean equals(Object anObject) {
equals メソッドはインスタンス・メソッドです。なぜなら、static として定義されていないからです。そのため、anObject パラメーターに加えて、CUT はメソッドを呼び出すオブジェクト・インスタンスからのデータも使用する可能性があります。
したがって、equals メソッドを呼び出した結果に影響を与える可能性のあるテスト・パラメーターが 2 つ存在します。それは、メソッドを呼び出すオブジェクト (this) とメソッド引数 (anObject) です。最初のパラメーターを a、メソッド引数 (anObject) を b と呼ぶとすると、コードでは a.equals(b) と表すことになります。ここで、equals メソッドをテストするには、a に関連付けたデータと b に関連付けたデータの両方を変化させてみればよいことが分かります。
これらの可能な入力 (a および b) は、両方ともオブジェクト・データ型です。最初の入力 (a) は常に Money の型であり、fCurrency および fAmount 属性によって定義されます。これらの定義属性は、Money クラス用の public コンストラクターのパラメーターです。したがって、最初の入力を変えるには、オブジェクトの定義属性 (fCurrency と fAmount) を変えます。負数からゼロ、さらに正数と値を変えながら、一定の範囲内の整数を指定してみましょう。その後、有効な通貨 ID と無効な通貨 ID を使用して、通貨タイプを変えてみます。
2 番目の入力 (b) はオブジェクト・データ型なので、事実上あらゆるクラス・インスタンスを有効値として渡せます。 Money オブジェクトを渡せば、意味のある比較を実行できます。しかし、比較するには無意味な他のデータ型のデータをあえて渡して、メソッドがどのように応答するかを確認することもできます。
データ区分は、最も多数の潜在的な欠陥が明らかになるような方法で定義する必要があります。この例では、入力パラメーターは 2 つあり、両方ともオブジェクト・データ型です。オブジェクトのパラメーター用の区分を定義するのに最適な方法は、それらの抽象状態を考慮することです。オブジェクトの抽象状態は、通常はそのオブジェクトの属性値に関する制約として定義されます。例えば、最初の入力パラメーター (a) の抽象状態としては、以下のものが考えられます。
ただし、このケースでは、equals メソッドが何を行うかを考えれば、最初のパラメーター (a) 用の区分は 1 つで十分です。なぜなら、このメソッドの戻り値は、このパラメーターの特定の値とは直接関連しない可能性が高いからです。結果的に、任意のテスト値を選択でき、データ区分は 1 つ作成するだけで済みます。後に等価クラスを定義する際には、このパラメーターの値をさまざまに変えてみることで、多様性に富んだテストを行えます。
2 番目の入力パラメーター (b) の場合は、抽象状態の定義は困難です。なぜなら、オブジェクト・パラメーター型はすべての型のスーパークラスであり、事実上あらゆるクラスを使用できるからです。このケースでは、最初にパラメーター型によってデータ区分を定義する必要があります。 (このことは、型が抽象クラスまたはインターフェースであるすべてのパラメーターにも当てはまります。) このパラメーターの場合は、型を次のように識別できます。
このように、入力パラメーター b 用として考えられるデータ区分には 「同じ型」、「その他」、および「ヌル」があります。
次に、2 番目の入力パラメーター (b) 用として、データ区分をさらに分割できます。互換性のない型のオブジェクトまたはヌル・オブジェクトの場合は、さらに分割する必要はまずありません。なぜなら、equals は意図的に false を戻すからです。
「同じ型」の場合は、次のような数種類の小区分への分割が考えられます。
次のステップは、等価クラスと値の定義です。等価クラスとは、そのどれもが同じ振る舞いを起動することを予期されている入力値のセットのことです。等価クラスの任意の単一値から失敗するテストが生成される場合は、その等価クラスのそれ以外のどの値を使用しても失敗するテストが生成されます。同様に、ある等価クラスの任意の単一値から成功するテストが生成される場合は、その等価クラスのそれ以外のどの値を使用しても成功するテストが生成されます。
Rational® Developer 製品の場合は、等価クラスをテスト・データ・テーブルに定義します。このテーブルでは、等価クラスは データ・セットと呼ばれます。先ほど定義したデータ区分を使用して、まず最初に、4 つの「同じ型」区分および「任意の属性値」区分の間の比較を扱う等価クラスを作成できます。以下の画面には、これらの 4 つの等価クラスのうちの 2 つを含むテスト・データ・テーブルが表示されています。
各等価クラスは、最初の入力パラメーター用として使用されたのと同じデータ区分 (「任意の属性値」) を再利用します。しかし、このテストでは多様性を持たせるために、それぞれの等価クラスで同じデータ区分のそれぞれ異なる値を使用することにします。
次は、以下の画面に表示されているように、ヌル・データの区分および非互換データ型の区分用の等価クラスを作成します。
最後に、以下の画面に表示されているように、セットと範囲を使用して、より多くのデータの組み合わせをテスト用に提供できます。金額として、負数、ゼロ、および正数を使用して、1 つの範囲の整数を指定します。通貨については、有効な通貨 ID と無効な通貨 ID のセットを使用します。これを行うと、テストで使用するデータの組み合わせ数が大幅に増えるため、結果的にテスト範囲が拡大され、欠陥を発見できる確率が高くなる可能性があります。
ただし、セットと範囲を使用する場合は、多数の個々のテストが作成されて、実行に長い時間がかかる可能性があることに注意してください。
この例では、メソッドのテストのためにデータ区分と等価クラスを定義する方法について説明しました。 多くのメソッドを呼び出すシナリオを定義する場合、またはメソッドがオブジェクトを使用する場合は、これと同じ手順を使用できます。ただし、これらのケースでは、扱える程度にまで問題を切り分けられるように、入力パラメーターの数を削減する必要があります。
これらのケースでは、オブジェクトの管理に抽象状態を使用することが非常に重要となります。オブジェクトの定義属性を区分化する場合は必ず、その前に抽象状態を定義しておく必要があります。当初はオブジェクトを 1 つの入力パラメーターと見なしていたとしても、最後には入力パラメーター数が何ダースにもなるというのはよくあることです。
これは、複雑なシナリオを扱う場合、入力パラメーターの選択にはかなりの注意深さが必要であるということです。一般的に、この複雑さを処理できる程度にとどめるには、問題を削減して、変数を 10 個未満に抑えるようにしてください。