概要
メソッドの内部で生成(new
)したローカル変数を, スタブに入れ替える.
問題のコード
次のようなコードがあるとする.
/// <summary>
/// ポート (ポートAとポートBの2つがある)
/// </summary>
enum Port
{
PortA,
PortB
}
interface IMotor
{
int Speed { get; set; }
}
class Motor : IMotor
{
/// <summary>
/// 現在の速度を取得または設定する
/// </summary>
public int Speed
{
get
{
// このコードは, 実機にモーターが
// 繋がっていないと動作しない.
// (テスト環境では動作させることはできない)
}
set
{
// このコードも上と同様, テスト環境では動かせない
}
}
/// <summary>
/// 指定したポートのモーターに接続し, Motorインスタンスを作成するコンストラクタ
/// </summary>
public Motor(Port port)
{
// モーターへの接続処理 (テスト環境では動作させることはできない)
// 今回は省略
}
}
class SampleClass
{
/// <summary>
/// テスト対象のメソッド
/// </summary>
public void SampleMethod()
{
IMotor motor = new Motor(Port.PortA);
int currentSpeed = motor.Speed;
// currentSpeedを取得して何か処理する.
// 省略する.
}
}
このコードは, 特別なハードウェア(以降, 実機と呼ぶ)上で実行するとする.
実機には, ポートAとポートBにモーターが接続されており, Motor
コンストラクタの引数に指定してインスタンスを作成することにより, そのポートのモーターを制御できるようになる(とする).
しかし, モーターが接続されていないテスト環境では, Motor
クラスのコンストラクタやメソッドは動作させることはできない.
このコードでは, SampleMethod
メソッドの中で, Speed
プロパティを呼んで, currentSpeed
の値を取得している.
インスタンスをIMotor
で持ち, IMotor.Speed
を呼んでいるあたり, 前回の"自分が作ったクラスのインスタンスメソッドをスタブに置き換える"の"インタフェースに依存させる"ということを意識して作られている.
しかし前回の記事と違うのは, motor
は引数で渡されるのではなく, メソッド内で作成しているところである.
メソッド内でnew Motor(Port.PortA)
と書いてしまっては, いくらIMotor
で受けようと, 中身は結局Motor
インスタンスである.
以降で, これをスタブに入れ替える方法を説明する.
インスタンスの生成を行う部分をメソッド化
"入れ替える方法を説明する"といっても, やるのは"静的メソッドのスタブ置き換え"で説明した"メソッドを格納できる変数"を使用するだけだ.
"メソッドを格納できる変数"に, Motor
インスタンスを作成する部分をメソッド化して格納しておく.
Motor
インスタンスを作成する部分は, 特定のインスタンスに関わらずインスタンスを作成できるようにするため, Motor
クラスの静的メソッドとして定義することにする.
class Motor : IMotor
{
// ~ 中略 ~
/// <summary>
/// Motorインスタンスを作成するメソッドを格納する変数
/// </summary>
public static Func<Port, IMotor> CreateMotor = CreateMotorBody; // 下に定義してあるメソッドを格納する
/// <summary>
/// Motorインスタンスを作成するメソッド
/// </summary>
private static IMotor CreateMotorBody(Port port)
{
return new Motor(port);
}
}
class SampleClass
{
/// <summary>
/// テスト対象のメソッド
/// </summary>
public void SampleMethod()
{
// メソッドを格納する変数を通して, インスタンス作成メソッドを実行
IMotor motor = Motor.CreateMotor(Port.PortA);
int currentSpeed = motor.Speed;
// currentSpeedを取得して何か処理する.
// 省略する.
}
}
スタブに入れ替えてみる
まず, スタブのクラスを定義する.
もちろん, IMotor
を実現させる.
スタブのインスタンスを作成するメソッドも定義しておく.
class MotorStub : IMotor
{
public int Speed
{
get
{
// ただ定数値を返す
return 0;
}
set
{
// 何もしない
}
}
/// <summary>
/// スタブ用Motorインスタンスを作成するメソッド
/// </summary>
public static IMotor CreateMotorBody(Port port)
{
return new MotorStub();
}
}
次に, SampleMethod
メソッドをテストするコードを示す.
[TestClass]
public class SampleClassTest
{
[TestMethod, Description("SampleMethodメソッドのテスト")]
public void SampleMethodTest()
{
// Arrange (準備)
SampleClass instance = new SampleClass();
Motor.CreateMotor = MotorStub.CreateMotorBody; // インスタンス生成メソッドを入れ替える
// Act (実行)
instance.SampleMethod();
// Assert (検証)
// 何か処理が行われたことを確認する
// 今回は省略
}
}
これで, SampleMethod
メソッド内では, インスタンス生成時にMotorStub.CreateMotorBody
メソッドを使ってインスタンスを生成するようになる.
さて, "静的メソッドのスタブ置き換え"の"静的な変数の注意点"でも述べたように, 静的メソッドの値を入れ替えると, インスタンスが破棄されても入れ替わったままである.
テストメソッドが終わったときに静的メソッドの値が元に戻るようにするには, 次のようにするとよいだろう.
[TestClass]
public class SampleClassTest
{
/// <summary>
/// もともとのMotor.CreateMotorの値を退避させておくための変数
/// </summary>
private Func<Port, IMotor> originalMethod;
[TestInitialize] // ひとつのテストが始まる前に呼ばれるメソッド
public void InitializeTest()
{
originalMethod = null;
}
[TestCleanup] // ひとつのテストが終わるたびに呼ばれるメソッド
public void CleanupTest()
{
if (originalMethod != null)
{
// 実行が終わったら, もとのメソッドに戻す
Motor.CreateMotor = originalMethod;
}
}
[TestMethod, Description("SampleMethodメソッドのテスト")]
public void SampleMethodTest()
{
// Arrange (準備)
SampleClass instance = new SampleClass();
Motor.CreateMotor = MotorStub.CreateMotorBody; // インスタンス生成メソッドを入れ替える
// Act (実行)
instance.SampleMethod();
// Assert (検証)
// 何か処理が行われたことを確認する
// 今回は省略
}
}
さらなる改善を求めて
例によって, 前述までのコードは次のような改善点が考えられる.
-
Motor.CreateMotor
を,public
なフィールドでなく, Getterのみのプロパティにする. - インスタンス生成メソッドの定義を, ラムダ式を使って簡略化
そして例によって, 説明は省略する.
余裕があれば記事書くかも.