SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

C#で始めるテスト駆動開発入門

状態を持つクラスをテストファーストする

C#で始めるテスト駆動開発入門(10)

ダウンロード CsTdd10.zip (122.5 KB)

CanExecuteChangedイベント

 これが最後になります。コマンドのCanExecuteプロパティが変化したときには、CanExecuteChangedイベントを発火させねばなりません。コマンドがバインドされているコントロールは、このイベントを受け取ると、コマンドのCanExecuteプロパティを読み取って自身の表示(実行可能か否か)を変更します。

 CanExecuteプロパティは外部から直接変更できませんが、Numberプロパティを使って間接的に変更できます。これも、前述の戦略2.ですね。テストコードは、PropertyChangedイベントと同じような方法で書けます(今度は発火するイベントが1種類だけなので、より簡単です)。次の製品コードは、膨れ上がってしまったNumberプロパティのsetterをリファクタリングした後のものです。

テストコード: GoNextCommand/GoPreviousCommandのCanExecuteChangedイベントのテスト
// GoNextCommand.CanExecuteChangedイベントを実装するためのテストコード

bool isCanExecuteChanged01Fired;

void cmd_CanExecuteChanged01(object sender, System.EventArgs e)
{
  isCanExecuteChanged01Fired = true;
}

[TestMethod]
[Timeout(100)]
public async Task GoNextCommand_CanExecuteChangedTest01_trueからfalse()
{
  FizzBuzzViewModel vm = new FizzBuzzViewModel();
  // GoNextCommand.CanExecute は既定で true

  vm.GoNextCommand.CanExecuteChanged += cmd_CanExecuteChanged01;

  vm.Number = "x"; // これで CanExecute が false になったので、発火するはず

  while (!isCanExecuteChanged01Fired)
    await Task.Delay(10);

  // → Numberのsetterに this.GoNextCommand.RaiseCanExecuteChanged(); を追加
}

[TestMethod]
public async Task GoNextCommand_CanExecuteChangedTest02_trueからtrue()
{
  FizzBuzzViewModel vm = new FizzBuzzViewModel();
  // GoNextCommand.CanExecute は既定で true

  vm.GoNextCommand.CanExecuteChanged += cmd_CanExecuteChanged01;

  vm.Number = "2"; // CanExecute は true のままので、発火しないはず

  // 十分な時間を待機
  await Task.Delay(100);

  Assert.IsFalse(isCanExecuteChanged01Fired);

  // → NumberのsetterにgoNextローカル変数を追加
  //    メンバー変数_canGoNextに直接代入せず、いったんgoNextで受け、
  //    true/falseが変化したときだけCanExecuteChangedを発火させるように。
}



// GoPreviousCommand.CanExecuteChangedイベントを実装するためのテストコード

[TestMethod]
[Timeout(100)]
public async Task GoPreviousCommand_CanExecuteChangedTest01_falseからtrue()
{
  FizzBuzzViewModel vm = new FizzBuzzViewModel();
  // GoPreviousCommand.CanExecute は既定で false

  vm.GoPreviousCommand.CanExecuteChanged += cmd_CanExecuteChanged01;

  vm.Number = "2"; // これで CanExecute が true になったので、発火するはず

  while (!isCanExecuteChanged01Fired)
    await Task.Delay(10);

  // → Numberのsetterに this.GoPreviousCommand.RaiseCanExecuteChanged(); を追加
}

[TestMethod]
public async Task GoPreviousCommand_CanExecuteChangedTest02_falseからfalse()
{
  FizzBuzzViewModel vm = new FizzBuzzViewModel();
  // GoPreviousCommand.CanExecute は既定で false

  vm.GoPreviousCommand.CanExecuteChanged += cmd_CanExecuteChanged01;

  vm.Number = "x"; // CanExecute は false のままので、発火しないはず

  // 十分な時間を待機
  await Task.Delay(100);

  Assert.IsFalse(isCanExecuteChanged01Fired);

  // → NumberのsetterにgoPreviousローカル変数を追加
  //    メンバー変数_canGoPreviousに直接代入せず、いったんgoPreviousで受け、
  //    true/falseが変化したときだけCanExecuteChangedを発火させるように。
}
製品コード: GoNextCommand/GoPreviousCommandのCanExecuteChangedイベントの実装
public string Number
{
  get { return _number; }
  set 
  {
    SetProperty<string>(ref _number, value);
    SetProperty<string>(ref _word, GetWord(), "Word");
    SetCanExecute();
  }
}

// (…略…)

private void SetCanExecute()
 {
   int n = GetNumber();
   SetCanGoNext(n);
   SetCanGoPrevious(n);
 }

#region GoNextCommand
public Common.RelayCommand GoNextCommand { get; private set; }

private bool _canGoNext = true;

private void SetCanGoNext(int number)
 {
   bool goNext = (0 < number) && (number < int.MaxValue);
   if (_canGoNext != goNext)
   {
     _canGoNext = goNext;
     this.GoNextCommand.RaiseCanExecuteChanged();
   }
 }
#endregion

#region GoPreviousCommand
public Common.RelayCommand GoPreviousCommand { get; private set; }

private bool _canGoPrevious = false;

private void SetCanGoPrevious(int number)
 {
   bool goPrevious = (1 < number);
   if (_canGoPrevious != goPrevious)
   {
     _canGoPrevious = goPrevious;
     this.GoPreviousCommand.RaiseCanExecuteChanged();
   }
 }
#endregion

 これでViewModelは完成です。出来上がったFizzBuzzViewModelクラスの全体は、サンプルソースをダウンロードしてご覧ください。

画面にバインドする

 出来上がったViewModelを画面と結び付けましょう。次のコードのようになります。

製品コード: FizzBuzzControlクラス(画面)にFizzBuzzViewModelクラス(ViewModel)をバインドした
<UserControl
    (…略…)
  >

  <!-- ViewModelのインスタンス -->
  <UserControl.Resources>
     <local:FizzBuzzViewModel x:Key="FizzBuzzViewModel" />
   </UserControl.Resources>

  <!-- ViewModelのインスタンスをDataContextに設定 -->
  <Grid (…略…)
        DataContext="{StaticResource FizzBuzzViewModel}"
        >

    <!-- (…略…) -->

    <!-- 戻るボタンのCommandプロパティには、ViewModelのGoPreviousCommandをバインド -->
    <AppBarButton Icon="Previous" IsCompact="True"
                  Command="{Binding GoPreviousCommand}"/>
    <!-- (…略…) -->

    <!-- 数字のTextBoxのTextプロパティには、ViewModelのNumberプロパティを双方向バインド -->
    <TextBox Text="{Binding Number, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
             MinWidth="60" TextAlignment="Right" />
    <!-- (…略…) -->

    <!-- 進むボタンのCommandプロパティには、ViewModelのGoNextCommandをバインド -->
    <AppBarButton Icon="Next" IsCompact="True"
                  Command="{Binding GoNextCommand}" />
    <!-- (…略…) -->

    <!-- 結果のTextBlockのTextプロパティには、ViewModelのWordプロパティを一方向バインド -->
    <TextBlock FontSize="60" Margin="10,10,0,0"
               Text="{Binding Word}" />

    <!-- (…略…) -->
  </Grid>
</UserControl>

 どのコントロールにも名前が付いていないこと、また、イベントも定義されていないことにご注目ください。それは、コードビハインドからコントロールにアクセスしていないことを示しています(実際、サンプルコードの「UI」フォルダーにある「FizzBuzzControl.xaml.cs」ファイルを見てもらうと、コンストラクター以外は何もありません)。

 しかしこれで、期待通りに動作します。数字を書き換えると結果が変わります。ボタンをタップすると、数字が変わるとともに、結果も変化します。数字によって、ボタンの実行可/不可も変化します。

 このように、ViewModelを作って画面からきれいに分離できることが分かります。そして、ViewModelはUIを持っていないので、TDDで作るのに適しています(もっとも、コマンドを実装するために書いたテストの数を考えると、コマンドを実装する必要があるのか(イベントハンドラーで構わないのでは)と思う人もいるでしょう)。

まとめ

 状態を持っているクラスの例として、ユニバーサルWindowsアプリのViewModelをテストファーストで作りました。状態と無関係である単純なメソッドと違って、状態(および、状態に依存するパーツ)は単独で作れないことが多いので、テストファーストする戦略を考える必要がありました。

 まず状態と作りたいパーツの依存関係を明確にすると、理解しやすくなります。そして、依存関係の少ないものから複雑なものという順序で作っていくとよいでしょう。

 スペック(外部設計)の観点から、状態をpublicにしてしまうのは最後の手段です。その前に、次の戦略を試してみます。

  1. スペック上、状態へのアクセスが必要な場合:すなおにテストが書けるはずです
  2. スペック上、状態の変化が別に観測可能な場合:変更と観測のペアをまとめてテストファーストします
  3. スペック上、状態の変化が観測できない場合:「プローブ」の挿入を検討します

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
C#で始めるテスト駆動開発入門連載記事一覧

もっと読む

この記事の著者

biac(ばいあっく)

HONDA R&Dで自動車の設計をやっていた機械屋さんが、技術の進化スピードに魅かれてプログラマーに。以来30年ほど、より良いコードをどうやったら作れるか、模索の人生。わんくま同盟の勉強会(名古屋)で、よく喋ってたりする。2014/10~2019/6 Microsoft MVP (Windows Devel...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/8238 2014/11/12 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー