概要
オーバーライドしていないvirtualでないメソッドや, 継承元の無いsealedなクラスなどの動作を, スタブに置き換える.
問題のコード
class Motor // このクラスはライブラリとかで用意されているものとする
{
/// <summary>
/// モーターの回転速度を取得する
/// </summary>
public int GetSpeed()
{
// このコードは, 実機にモーターが
// 繋がっていないと動作しない.
// (テスト環境では動作させることはできない)
}
}
class SampleClass // 自分で作ったクラス
{
/// <summary>
/// テスト対象のメソッド
/// </summary>
/// <param name="motor">処理したいモーター</param>
public void SampleMethod(Motor motor)
{
int currentSpeed = motor.GetSpeed();
// currentSpeedを取得して何か処理する.
// 省略する.
}
}
このコードは, 特別なデバイス(実機)上で動作させるものとする.
実機にはモーターが接続されており, Motor
クラスを通じて制御をおこなう.
このSampleMethod
メソッドのテストを作成し, CIツール(Travis CIとかAppVeyorとか)で自動テストさせたい.
でも, CIツールの環境では, モーターが接続されていないので, GetSpeed()
メソッドは動作しない.
テストの時は, GetSpeed()
は呼ばず, 代わりのメソッドを呼ぶようにしたい.
ここまでであれば, 自分が作ったクラスのインスタンスメソッドをスタブに置き換えるのと同じである.
しかしここでは, 上記の記事とは異なり, Motor
クラスは自分で作ったものでなくライブラリなどで提供されているもので, 自分では手を入れることができないものとする.
つまり, 上記の記事にあるような"インタフェースに依存させる"ということができない.
また, virtual
が付いてないので, Motor
クラスを継承してGetSpeed()
の動作を上書き(オーバーライド)することもできない.
以降で, Motorインスタンスをスタブに置き換えて, SampleMethodメソッドをテストできるようにしてみる.
ラップするクラスを作る
インタフェースに依存させることもできず, 動作をオーバーライドすることもできないともあれば, もうしょうがないのでMotor
をラップするクラスを作るぐらいしかない.
具体的には次のようにする.
class MotorWrapper
{
public MotorWrapper()
{
Motor instance = new Motor();
GetSpeed = instance.GetSpeed;
}
public Func<int> GetSpeed{ get; }
}
こちらで説明した, "メソッドを格納できる変数"を利用している.
使う側では, 次のようになるだろう.
class SampleClass
{
/// <summary>
/// テスト対象のメソッド
/// </summary>
/// <param name="motor">処理したいモーター</param>
public void SampleMethod(MotorWrapper motor)
{
int currentSpeed = motor.GetSpeed();
// currentSpeedを取得して何か処理する.
// 省略する.
}
}
MotorWrapper
インスタンスを受け取れば, あとはMotor
のときと同じようにGetSpeed()
を実行できる.
スタブに入れ替えてみる
では, テストコードで, GetSpeed()
メソッドの動作を入れ替えてみる.
[TestClass]
public class SampleClassTest
{
[TestMethod, Description("SampleMethodメソッドのテスト")]
public void SampleMethodTest()
{
// Arrange (準備)
SampleClass instance = new SampleClass();
MotorWrapper inputMotor = new MotorWrapper();
inputMotor.GetSpeed = GetSpeedStub; // GetSpeedの動作を入れ替える
// Act (実行)
instance.SampleMethod(inputMotor);
// Assert (検証)
// 何か処理が行われたことを確認する
// 今回は省略
}
/// <summary>
/// Motor.GetSpeedメソッドのスタブメソッド
/// </summary>
private int GetSpeedStub()
{
// 何もしない
return 0;
}
}
もうちょっと改善
ラムダ式を知っていると, もうちょっと簡単に入れ替えられる.
inputMotor.GetSpeed = () => 0; // GetSpeedの動作を入れ替える
おしまい.
"メソッドを格納する変数"を使うと, Intellisenseによるメソッドの説明が出てこなくなるのがつらい.
インタフェースを用意していないライブラリを恨むしかないのか...