PropertyChangedイベント
イベントも単独ではテストできません。今回のPropertyChangedイベントは、状態の変化によって発火しますから、これも前述の戦略2.によって、読み書き可能なNumberプロパティと組み合わせればテストコードが書けます。
外部から変更できるプロパティはNumberだけですが、Wordプロパティも同時に変化します。従って、PropertyChangedイベントはプロパティ2つ分(=2回)発行されます。
PropertyChangedイベントは、INotifyPropertyChangedインターフェースの実装として作ります。そこで、まずINotifyPropertyChangedインターフェースからテストファーストします。
[TestMethod]
public void PropertyChangedTest01_INotifyPropertyChangedを実装()
{
FizzBuzzViewModel vm = new FizzBuzzViewModel();
Assert.IsInstanceOfType(vm,
typeof(System.ComponentModel.INotifyPropertyChanged));
}
public class FizzBuzzViewModel : System.ComponentModel.INotifyPropertyChanged
{
// (…略…)
#region INotifyPropertyChangedの実装
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
#endregion
}
ここでちょっと便利なコードを、TDDせずに追加しておきます。ViewModelでは、PropertyChangedイベントを発火させるコードを何度も書くことになるので、そこをサポートするメソッドです。これは、Visual Studio 2012の「グリッド アプリケーション (XAML)」テンプレートや「分割アプリケーション (XAML)」テンプレートのCommonフォルダに入っている「BindableBase」クラスからコピーしてきたコードです(一部改良済み)。
#region INotifyPropertyChangedの実装
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
private bool SetProperty<T>(ref T storage, T value,
[System.Runtime.CompilerServices.CallerMemberName]
string propertyName = null)
{
if (object.Equals(storage, value)) return false;
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
private void OnPropertyChanged(
[System.Runtime.CompilerServices.CallerMemberName]
string propertyName = null)
{
var eventHandler = this.PropertyChanged;
if (eventHandler != null)
{
eventHandler(this,
new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
#endregion
それでは、PropertyChangedイベントを発火させる部分を作りましょう。イベントをテストするコードは、ちょっと工夫が必要です。テストメソッド中でイベントハンドラーをセットし、イベントの発火を待ちます。発火したら、イベントハンドラー内でそのことをメンバー変数に記録し、元のテストメソッドでメンバー変数を検査します。しばらく待っても発火しなかったらREDにします。前回紹介したように、async/awaitを使ったテストが簡単に書けるようになったので、このようなテストも比較的簡単に書くことができます。
[TestMethod]
[Timeout(100)]
public async Task PropertyChangedTest02_違う数字に変えると2回発火する()
{
// ファイル冒頭に using System.Threading.Tasks; を追加
FizzBuzzViewModel vm = new FizzBuzzViewModel();
// テスト用のイベントハンドラーをアタッチ
vm.PropertyChanged += vm_PropertyChanged01;
// Numberプロパティを変更する(これでイベントが発火するはず)
vm.Number = "15";
// イベント発生を待機
while (!_isPropertyChanged01NumberSuccess && _propertyChanged01Fail == null)
await Task.Delay(10);
while (!_isPropertyChanged01WordSuccess && _propertyChanged01Fail == null)
await Task.Delay(10);
// 想定通りのイベントが発生しなかったら、タイムアウトでREDになる
// 想定外のプロパティ名で発火していたらRED
if (_propertyChanged01Fail != null)
throw _propertyChanged01Fail;
}
bool _isPropertyChanged01NumberSuccess;
bool _isPropertyChanged01WordSuccess;
System.Exception _propertyChanged01Fail;
void vm_PropertyChanged01(object sender,
System.ComponentModel.PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "Number":
_isPropertyChanged01NumberSuccess = true;
break;
case "Word":
_isPropertyChanged01WordSuccess = true;
break;
default:
_propertyChanged01Fail
= new AssertFailedException("想定外のプロパティ名:" + e.PropertyName);
break;
}
}
製品コードのほうは、先ほど追加したヘルパーメソッドを利用して、Numberプロパティのsetterを次のように変更するだけです。
public string Number
{
get { return _number; }
set
{
//_number = value;
SetProperty<string>(ref _number, value);
//_word = GetWord();
SetProperty<string>(ref _word, GetWord(), "Word");
}
}
Nextメソッド/Previousメソッド
続いては2つのコマンド(GoNextCommand/GoPreviousCommand)を作りたいところですが、コマンドを作るのは複雑な作業です。いっぺんに作るのではなく、まずはコマンドで実行される処理の部分から作っていきましょう。
それぞれのコマンドで実行される処理を、Nextメソッド/Previousメソッドとします。これらのメソッドはpublicでよいでしょう。スペックは次のようになります。
| 作るもの |
Nextメソッド |
||||||
|---|---|---|---|---|---|---|---|
| 置き場所 |
FizzBuzzViewModelクラス |
||||||
| 引数 | (なし) | ||||||
| 返値の型 |
void |
||||||
| 反応 |
CanExecuteプロパティで制限するので、ここでは気にしない |
| 作るもの |
Previousメソッド |
||||||
|---|---|---|---|---|---|---|---|
| 置き場所 |
FizzBuzzViewModelクラス |
||||||
| 引数 | (なし) | ||||||
| 返値の型 |
void |
||||||
| 反応 |
CanExecuteプロパティで制限するので、ここでは気にしない |
これも前述の戦略2.ですね。メソッドを呼び出すと状態が変わる。その状態はスペック的に外部から観測可能。次のようなテストコードと製品コードになります。
[TestMethod]
public void NextTest01_数字のとき()
{
FizzBuzzViewModel vm = new FizzBuzzViewModel();
vm.Number = "4";
vm.Next();
Assert.AreEqual<string>("5", vm.Number);
}
[TestMethod]
public void NextTest02_数字以外の文字列のとき()
{
FizzBuzzViewModel vm = new FizzBuzzViewModel();
vm.Number = "x";
vm.Next();
Assert.AreEqual<string>("x", vm.Number);
}
[TestMethod]
public void PreviousTest01_数字のとき()
{
FizzBuzzViewModel vm = new FizzBuzzViewModel();
vm.Number = "4";
vm.Previous();
Assert.AreEqual<string>("3", vm.Number);
}
[TestMethod]
public void PreviousTest02_数字以外の文字列のとき()
{
FizzBuzzViewModel vm = new FizzBuzzViewModel();
vm.Number = "x";
vm.Previous();
Assert.AreEqual<string>("x", vm.Number);
}
public void Next()
{
int n = this.GetNumber();
if (n > 0)
this.Number = (n + 1).ToString();
}
public void Previous()
{
int n = this.GetNumber();
if (n > 0)
this.Number = (n - 1).ToString();
}
// ここでthis.Numberではなく、メンバー変数_numberに代入するコードを書いてしまった場合は、
// Next/Previous呼び出しでもPropertyChangedイベントが発火するというテストを書く
