状態を持つクラスをテストファーストする戦略
クラスが状態を持っていないのならば、そのメソッドは素直にテストファーストで作れます。そのスペックは、第1回で行ったようにメソッドのシグネチャと反応を網羅するだけで済みます。
クラスが状態を持つ場合、その状態を持つメンバー変数は一般には隠蔽しますから、スペック(外部設計)にそのメンバー変数は登場しません。つまり、クラスが持つ状態やその状態に依存するメソッドをテストするのは難しいのです。そこで筆者は、次のような戦略で対処しています。
-
スペック上、状態へのアクセスが必要な場合:
例えば、TextBoxコントロールのTextプロパティは、(直接ではないにしても)状態の読み書きをスペックとして可能にしています。このような場合は、そのプロパティだけをテストファーストで作ることができます。 -
スペック上、状態の変化が観測可能な場合:
状態を変更するメソッドやプロパティと、状態を観測できるメソッドやプロパティが分かれている場合です。例えば、StackクラスのPushメソッドとPeekメソッドなどです。このような場合は、変更と観測のペアをまとめてテストファーストで作ります。 -
スペック上、状態が観測できない場合:
そのような場合でも、状態が観測できないとテストできないときがあります。例えば「インスタンス化時の時刻を記憶し、そのミリ秒の上2桁を返すプロパティ(乱数の代用)」といったような場合です。このようなときには#ifディレクティブを使って、テストのときだけスコープを変えたり、プローブ注4を製品コードに挿入したりします(具体例は後述)。そのほか、リフレクションを利用してprivateメンバーにアクセスすることも可能ですが、手間が掛かるので、製品コードがよほど読みづらくならない限り筆者は使っていません。
また、クラスの状態には、独立した状態と従属した状態があります。例えば、TextBoxコントロールのTextプロパティは、他の状態に影響されません。対して、Countプロパティは、Textプロパティに従属しています。Textプロパティを変更すると、Countプロパティが変化します。このようなときは、まず独立した状態をテストファーストで作り、その後で従属した状態に取り掛かるようにします。逆の順序では作れませんし、いっぺんに作ろうとすると、考慮すべきことが複雑になるので困難さが増してしまいます。
筆者の命名。探針。状態を探るためのメソッドを製品コードに挿入する技法。製品コードが煩雑になるため、必要なときに限って使います。なお、テストのための変更を製品に加えることは、ソフトウェアに限ったことではありません。ハードウェアの開発でも、例えば試験装置に固定するための「出っ張り」を機械部品に付加したり、テスト用の電気信号を取り出すためのピン(端子)を電子回路に追加したりといったことが行われています。テストできない(=スペックを満たしていると確認できない)モノは出荷できませんから。
補足:スペック上は状態が観測できない場合の例
先述した「インスタンス化時の時刻を記憶し、そのミリ秒の上2桁を返すプロパティ(乱数の代用)」をテストファーストする例です。
まず、スコープを変えてしまう方法から。テストコードと製品コードを示します。
[TestClass]
public class スコープを変えるサンプルTest
{
#if DEBUG
[TestMethod]
public void InstanceTest()
{
var o = new CsTdd08App.スコープを変えるサンプル();
// アサート:インスタンス化時に何か日時がセットされているはず
// (デバッグビルド時はアクセス可能)
Assert.IsTrue(DateTime.MinValue < o._startTime);
}
[TestMethod]
public void RandomNumberTest()
{
var o = new CsTdd08App.スコープを変えるサンプル();
// 値をセット(デバッグビルド時はアクセス可能)
o._startTime = new DateTime(2014, 10, 10, 1, 23, 45, 678);
// アサート:セットした値を使った結果になるはず
Assert.AreEqual<int>(67, o.RandomNumber);
}
#endif
}
public class スコープを変えるサンプル
{
// このメンバー変数は、リリースビルド時にprivateになる
#if DEBUG
public
#else
private
#endif
DateTime _startTime = DateTime.Now;
public int RandomNumber
{
get
{
return _startTime.Millisecond / 10;
}
}
}
上のスコープを変える方法は、製品コードがご覧のとおりに読みづらくなります。次に示すプローブによる方法は、これに比べると読みやすいです。テスト用であることを示すため、「test_StartTime」という変なプロパティ名にしています。
[TestClass]
public class プローブを使うサンプルTest
{
#if DEBUG
[TestMethod]
public void InstanceTest()
{
var o = new CsTdd08App.プローブを使うサンプル();
// アサート:インスタンス化時に何か日時がセットされているはず
// プローブを使ってDateTime.MinValue以外の値になっていることを確認する
Assert.IsTrue(DateTime.MinValue < o.test_StartTime);
}
[TestMethod]
public void RandomNumberTest()
{
var o = new CsTdd08App.プローブを使うサンプル();
// プローブから値をセット
o.test_StartTime = new DateTime(2014, 10, 10, 1, 23, 45, 678);
// アサート:プローブからセットした値を使った結果になるはず
Assert.AreEqual<int>(67, o.RandomNumber);
}
#endif
}
public class プローブを使うサンプル
{
private DateTime _startTime = DateTime.Now;
public int RandomNumber
{
get
{
return _startTime.Millisecond / 10;
}
}
#if DEBUG
#region テスト時だけ利用するコード
// このように下端に固めておくと、製品コードを読むときの邪魔にならない
public DateTime test_StartTime
{
get { return _startTime; }
set { _startTime = value; }
}
#endregion
#endif
}
上のプローブの例は、メンバー変数にアクセスするだけの簡単なものです。その他に、例えば任意の場所で例外を出したり、コード中の特定箇所を実行した回数をカウント注5したりなどと、さまざまな目的に利用できます。
例えば、ソートするメソッドを作るとき、想定したアルゴリズムで動作しているかを確認するために、データを比較した回数を検査しなければならない場合があります。とりあえずソートできるメソッドをテストファーストで作り、次いで、「想定しているアルゴリズムにこのデータを与えたならばX回の比較で済むはず」というテストを書いてREDにし、アルゴリズムを作り込むわけです。
