NUnitでBDD - コンテキストのリファクタリング (^_^)

TDDでは実装クラスとテストクラスの関係が1対1になる事が多く、1つのテストクラス内に複数のコンテキストが混在しやすい。テストクラスのSetUpで定義したテストの事前条件がテストメソッド毎に上書きされていたりするような場合がそうだ。

これではテストクラスが大きく肥大してしまい、テストメソッドも複雑になって意図が分かりにくくなってしまう。

BDD的なテストではコンテキスト毎にテストクラスを作成するため、同じテストクラス内の全てのテストメソッドの事前条件を共通化できる。以下にStackのテストの例を示す。


[TestFixture] public class オブジェクトを1つプッシュする
{
 private Stack target_;
 private string pushed_ ="first element";
 [SetUp] public void 事前条件()
 {
  target_ = new Stack();
  target_.Push(pushed_);
 }
 ...
}

[TestFixture] public class 複数のオブジェクトをプッシュする
{
 private Stack target_;
 private string pushed1_ = "1";
 private string pushed2_ = "2";
 private string pushed3_ = "3";
 [SetUp] public void 事前条件()
 {
  target_ = new Stack();
  target_.Push(pushed1_);
  target_.Push(pushed2_);
  target_.Push(pushed3_);
 }
 ...
}

しかし、コンテキスト毎にテストクラスを作成して共通の事前条件でテストを開始しても、さらにオブジェクトの状態を変化させる必要があるケースも出てくる。例えば次のような感じになるだろう。


[TestFixture] public class あるコンテキスト
{
 private Foo target_;
 [SetUp] public void 事前条件()
 {
  target_ = new Foo();
  ...
 }
 [Test] public void ○○○になるべき()
 {}
 ...
 [Test] public void 状態をAに変化させると△△△になるべき()
 {
  target_の状態をAにするコード;
  ...
 }
 [Test] public void 状態をAに変化させて〜すると□□□になるべき()
 {
  target_の状態をAにするコード;
  ...
 }
 ...
}

下の2つのテストメソッドは、事前条件は他のテストメソッドと同じだがメソッド内で共通のコンテキストの変化が見られる。これはテストクラスの中でコンテキストが入れ子状態になっていると考えてもよい。

この様な場合には、コンテキストのリファクタリングを行う。

以下にその手順を示す。

1.新たなテストクラスをインナークラスとして作成する。テストクラスには変化するコンテキストの名前を付ける。この際には外枠のテストクラスの事前条件もコピーしておく。


[TestFixture] public class あるコンテキスト
{
 private Foo target_;
 [SetUp] public void 事前条件()
 {
  target_ = new Foo();
  ...
 }
 // インナークラスを作成して外枠の事前条件をコピーする。
 [TestFixture] public class 状態をAに変化させる
 {
  private Foo target_;
  [SetUp] public void 事前条件()
  {
   target_ = new Foo();
   ...
  }
 }

 [Test] public void 状態をAに変化させると△△△になるべき()
 {
  オブジェクトの状態をAにするコード;
  ...
 }
 [Test] public void 状態をAに変化させて〜すると□□□になるべき()
 {
  オブジェクトの状態をAにするコード;
  ...
 }
 ...
}

2.メソッドの移動を行い、外枠のテストから新しく作成したインナークラスへ対象のテストメソッドを移す。


[TestFixture] public class あるコンテキスト
{
 private Foo target_;
 [SetUp] public void 事前条件()
 {
  target_ = new Foo();
  ...
 }
 ...
 [TestFixture] public class 状態をAに変化させる
 {
  private Foo target_;
  [SetUp] public void 事前条件()
  {
   target_ = new Foo();
   ...
  }
  // 移動されたテストメソッド
  [Test] public void 状態をAに変化させると△△△になるべき()
  {
   オブジェクトの状態をAにするコード;
   ...
  }
  [Test] public void 状態をAに変化させて〜すると□□□になるべき()
  {
   オブジェクトの状態をAにするコード;
   ...
  }

 }
 ...
}

3.各テストメソッドからコンテキストを変化させているコードをSetUpへ移動して重複を取り除き、メソッド名の変更を行う。


[TestFixture] public class あるコンテキスト
{
 private Foo target_;
 [SetUp] public void 事前条件()
 {
  target_ = new Foo();
  ...
 }
 ...
 [TestFixture] public class 状態をAに変化させる
 {
  private Foo target_;
  [SetUp] public void 事前条件()
  {
   target_ = new Foo();
   ...
   オブジェクトの状態をAにするコード; // 移動されたコード
  }
  // メソッド名を変更する。
  [Test] public void △△△になるべき()
  {
   ...
  }
  [Test] public void 〜すると□□□になるべき()
  {
   ...
  }
 }
 ...
}

以上で完了だ。これでコンテキストがより明確になり、テストメソッドはシンプルになった。

参考までに、NUnitで実行するとツリーには以下の様に表示される。インナークラスにした部分はNUnitが勝手に「外枠のクラス名+インナークラス名」として表示してくれる。

  • ほげほげのストーリー
    • あるコンテキスト
      • ○○○になるべき
    • あるコンテキスト+状態をAに変化させる
      • △△△になるべき
      • 〜すると□□□になるべき

インナークラスには最初は違和感があると思うが、別ファイルとして作成したテストクラスのような感覚で見るとよいだろう。ただしテストクラス全体としてコードの可読性が下がるのでインナークラス部分をregionで括ってしまうのもいいだろう。