2011年1月23日日曜日

GUI コードのUnitTest の基本形

こんなフォームがあるとする。


ツールボックスからコントロールを持ってきて置いただけの状態だが、これを
右のボタンを押すとメッセージボックスが開いて、OK すると文字列"OK"、Cancel すると文字列"Cancel"が、左のテキストボックスに表示される
ようにしたい。

さて、この作業を TDD でやるとどうなるか。

GUI アプリは UnitTest できないものと考えがちな人は、やはりここでもメッセージボックスに伴うユーザ・インタラクションを理由に UnitTest を諦める。

実は、こういった場合の対処法は割と昔から研究されて、パターンとして広く知られているものも既にいくつかある。

基本的な方針は、最小限のコードを残して、振る舞いに関する責務をテスタビリティの高い別のオブジェクトに移動してしまうというもの。

そうやって、極薄にしたフォーム・コード(UnitTest はオプション)と、振る舞いを含む実質的な UI ロジック(カバレッジ100%)に分けることで UI 層全体としてのコード・カバレッジを満足のいくものにする。

いろいろな変種パターンがあるが、さしあたり以下のように単純に実装してみる。
  • Form1 = view
    • あくまでも受動的に動くフォーム・クラス。
    • Presenter の指示により MessageBox を表示するものの、何も考えずに結果を Presenter に返すだけ。
    • イベントは Presenter に丸投げする。ただし無駄なパラメータは渡したりしない。また、フォーム上のコントロールのプロパティへのアクセッサを、必要最小限な分だけもつ。
  • Form1Presenter = presenter
    • 次のような UI の振る舞いを担う。
      • View にメッセージボックスを表示させて、結果を受け取る。
      • 受け取った結果に応じた文字列を View に表示させる。

また、Presenter から Form1 へのアクセスは、以下のようなインターフェイスを通じて行う。
public interface IForm1
{
string ResultText { get; set; }
bool AskOkOrCancel();
}
※Presenter も interface を実装するような形にもできるが、オプション。

まずは先にテストコードから
[TestFixture]
public class Form1PresenterTest
{
private Form1Presenter presenter;
private Mock<IForm1> mock;

[SetUp]
public void SetUp()
{
this.mock = new Mock<IForm1>();
this.presenter = new Form1Presenter(mock.Object);
}
[Test]
public void HandleButtonClick_OK()
{
TestHandleButtonClick(true, "OK");
}
[Test]
public void HandleButtonClick_Cancel()
{
TestHandleButtonClick(false, "Cancel");
}
public void TestHandleButtonClick(bool isOk, string resultText)
{
//arrange
mock.Setup(v => v.AskOkOrCancel()).Returns(isOk);
//act
presenter.HandleButtonClick();
//assert
mock.VerifySet(v => v.ResultText = resultText);
}
}
※モックはMoqを使った
  • ビューに聞いてこさせたユーザの意図(isOk)に応じて、期待通りの文字列(resultText)が選択され、ビューに設定されることを検証している。
  • 見ての通り MessegeBox もコントロール・オブジェクトも関係ないコードになっている。
  • 実際には、いっぺんにテスト・コードを書いてしまわないで、ちょこっとテストを書いては本体コードを書くというような形になる。
で、その本体コードは以下
public class Form1Presenter
{
private readonly IForm1 view;
public Form1Presenter(IForm1 view)
{
this.view = view;
}
public void HandleButtonClick()
{
this.view.ResultText =
this.view.AskOkOrCancel() ? "OK": "Cancel";
}
}

ここまで書けば取りあえずテストが通る。あと残ったのは、放置していた Form1 を含む Presenter との連携コード。

こんな感じでフォームを実装する。
public partial class Form1 : Form, IForm1
{
private Form1Presenter presenter;

public Form1()
{
InitializeComponent();
}
public bool AskOkOrCancel()
{
return DialogResult.OK == MessageBox.Show(
"OK or Cancel", "", MessageBoxButtons.OKCancel);
}
public string ResultText
{
get { return this.textBox1.Text; }
set { this.textBox1.Text = value; }
}
internal void Attach(Form1Presenter presenter)
{
this.presenter = presenter;
}
private void button1_Click(object sender, EventArgs e)
{
this.presenter.HandleButtonClick();
}
}
この他、フォームを表示するところ(Program.csあたり)で、view を Presenter と関連付けるコードが必要だが割愛。

こんな風に、実フォームでは MessageBox.Show() 等のユーザ・インタラクションと、コントロールへの必要最小限のアクセッサのみを書く。

なんだか、もとのお題が小さすぎるから、コードを切り出したにもかかわらずフォームのコードが増えてしまっているが、実際の業務要件を満たすようなフォームだと、イベント・ハンドラに全部書くようなスタイルよりスッキリしたコードに、ちゃんとなる。

こうしたフォーム・コードは、コード量も多くならないし更新の度合いも少ないし、テストの必要性はそれほど高くない。UnitTest を省略していい部分だと思うが、どうしても DeveloperTest を書きたければ TypeMock か Moleを使えばいい。後付でも問題ないし、テスト・コードも極簡単なものですむ。

さらにフォームのコードを少なくするやり方もあるが、次の機会に。

1 件のコメント:

Yossy さんのコメント...

はじめまして。Yossyと申します。

このブログを読んで、ユーザインターフェースのユニットテストを容易にする設計を知りました。
今までは、例えば「○○ボタンが押された状態」というようにStateパターンを利用して、ユーザインターフェースの動作を状態で表現する設計をしてきました。
そのためにユーザインターフェースの動作が複雑になると、状態を表現するための設計が複雑になってしまい、苦労することが多かったです。
そんな中、このブログに行き当たり、コードを参考に自身の設計を再設計してみると、非常にすっきりした見通しの良い構造になりました。ありがとうございます。
今までの設計では状態を表現するクラスに、PresenterとIFormはDPでいうところのMediatorだと思いますが、知らないがゆえに複数の役割を割り当てていたために複雑な構造になっていました。
ユーザインターフェースの動作を表現するのにStateを使うのは目的違いだとは知っていましたが、私の周囲にはバチッと設計できる技術者がいないうえに、わかりやすい設計サンプルを目にすることもなかったので、模索しながらも今日まで来てしまった訳です。
IFormに相当するインターフェースを定義する技術はこれからですが、あなたのブログから素晴らしい設計を学べたことに感謝申し上げます。

コメントを投稿