« [コラム] Silverlight 2 / 3 でも、ユニットテスト出来る | トップページ | [NEWS] NUnit 2.5.3 リリース、 .NET 4.0 に対応 »

2009年12月13日 (日)

[コラム] VS2010b2J (MSTest) で、 Silverlight 3 のロジックを TDD する

[ summary ]
  1. Using the unit test feature (MSTest) of VS2010 beta2 (Japanese), it's possible to test the logic included in the Silverlight 3 project.
  2. As for the wizards who helps the unit test that VS2010b2J has, some functions don't work normally.
  3. The unit test can be executed though warning goes out by the project reference.
  4. After the unit test is executed, coverage can be neatly acquired.
  5. Logic of Silverlight 3 can be made by using TDD technique. I hope for the support of Visual Studio to be improved.

Visual Studio 搭載のユニット テスト (MSTest) や、 現在の NUnit は、 通常の .NET Framework のランタイム上で動作します。 Silverlight は、 異なるフレームワーク ファミリーのランタイム上で動作しますので、 互換性はありません。 ここで誤解されることがあるのですが (私も誤解していました)、 しかし呼び出しすらできないというわけではありません。 Silverlight  に依存するコードを .NET Framework から呼び出すとエラーになる、 という意味で非互換と言っているようです。

Visual Studio 2010 では、 標準で Silverlight 3 の開発ができます。 そして標準機能のみで、 Silverlight 3 のロジック部分はユニットテストできます。 この記事では、 そのことを実験し、またその手順を説明します。

※ 以下は、 Visual Studio 2010 beta2 日本語版での結果です。 正規版では異なる可能性があります。
※ なお、 Silverlight 4 beta に対しても、 ほぼ同様でした。

【 要約 】

  1. VS2010b2J のユニットテスト (MSTest) から、 SL3 のプロジェクトに含まれているロジックをテストすることは、 可能である。
  2. ユニットテストのために VS2010b2J が持っているウィザードは、 幾つかの機能が正常に動作しない。
  3. 参照設定で警告が出るものの、 問題無くテストできる。
  4. ユニットテスト実行時のカバレッジも、 ちゃんと取得できる。
  5. Silverlight 3 のロジックを TDD で作ることは可能である。 Visual Studio のサポートが改善されることが望まれる。

【 環境 】

※ 今回の実験には影響しないと思いますが、 次の Silverlight 4 の環境も入っています。

※ 動作に関係は無いと思いますが、 次のふたつも入っています。

【 1. Silverlight 3 のプロジェクトを作る 】

新しいプロジェクトを用意します。 「Silverlight アプリケーション」 を選び、 名前を付けます。 ここでは、 "SL3App01" というプロジェクト名にしました。
20091212_sl3_01
場所が D: ドライブになっていますが、 そこは Windows が入っているのと同じドライブです。

[OK] ボタンをクリックすると、 「Silverlight アプリケーション」 ダイアログが出てきます。
20091212_sl3_02
オプションの 「Silverlight のバージョン」 が 3.0 になっていることを確認し、 あとはデフォルトのまま [OK] をクリックして、 ソリューションとプロジェクトを生成させます。

Silverlight 3 のプロジェクト "SL3App01" と、 それをテストするための Web プロジェクト "SL3App01.Web" が自動生成されました。
20091212_sl3_03
画面に何も無いのでは面白くないので、 背景色を指定し、 ボタンを追加しました。
さらに、 ボタンのプロパティ ウィンドウで、 イベント タブを選び、 Click イベントに "ShowNowTime" メソッドを設定しました。 すると、 MainPage.xaml.cs に ShowNowTime() メソッドが自動生成されます。
20091212_sl3_04
ShowNowTime() では、 現在時刻を表示するようにしてみます。

private void ShowNowTime(object sender, RoutedEventArgs e)
{
    MessageBox.Show(FormatDateTime(DateTime.Now));
}

ここで、 FormatDateTime() メソッドは、時刻をフォーマットして文字列を返すメソッドです。 これをテストファーストで作ってみます。

【 2. テスト プロジェクトを作る 】

まずは、 Visual Studio の従来からのやり方でやってみましょう。 MainPage.xaml.cs に、 FormatDateTime() メソッドの仮実装を書きます。

internal string FormatDateTime(DateTime dt) {
    return null;
}

そうしたら、 このメソッド内にカーソルを置いたまま右クリックして、 [単体テストの作成...] を選びます。

20091212_sl3_05
出力プロジェクトの欄は新規作成のままにして、 新しくテスト プロジェクトを作らせます。 確認のために [設定...] ボタンをクリックして、 InternalVisibleTo 属性が追加されるようになっていることを、 見ておいてください。

[OK] ボタンをクリックすると、プロジェクト名を求められます
20091212_sl3_06
ここでは、 TestProject1 のままにしました。 しばらく待つと、 テストプロジェクトが生成されます。

20091212_sl3_07

ところが、 きちんと出来上がっていません。
・ SL3App01 への参照が付いていない。
・ テスト メソッドのスタブが作られていない。

テスト メソッドをゼロから書いてもたいした手間ではないので、 スタブが作られないことはあまり気になりません。 参照設定にテスト ターゲットのプロジェクトが入らないのは、 バグだと言いたくなります。
※ (2010/1/7 追記) 単体テストの作成機能は Silverlight をサポートしない (上記の挙動はバグではない)、 という公式回答が MS から出されています。 ⇒ [SL3/4] VS2010b2J のフィードバックを出しました

ビルドしてみると、 参照設定が足りないので当然ながらエラーになります。
20091212_sl3_08
そこで、 参照の設定をしてみます。
20091212_sl3_09
「参照の追加」 ダイアログに、 Silverlight 3 のプロジェクトが出てきます。 選択して [OK] します。

20091212_sl3_10
すると、 ソリューション エクスプローラー上では参照設定に追加されたものの、 黄色い三角の警告マークが付いてしまいます。
エラー一覧を見てみると…
20091212_sl3_11
「警告 1 プロジェクト 'SL3App01' を参照できません。参照されるプロジェクトは、異なるフレームワーク ファミリー (Silverlight) を対象としています」 と、 いかにも .NET Framework から Silverlight を呼び出すことはできないかのような警告が出ています。
でも、 コードの 1行目、 using しているところから赤波線が消えていることに注目してください。 実際には参照できているようです。

テストメソッドを書いてみます。

[TestMethod()]
public void FormatDateTimeTest01_AM() {
    MainPage page = new MainPage();
    page.

…と、 ここまで打ったところで。 インテリセンスに、 テスト ターゲットのメソッド FormatDateTime() が出てきません。
これは、 InternalVisibleTo 属性が自動的にセットされなかった可能性が高いです。 ターゲット プロジェクトの AssemblyInfo.cs を見てみます。
20091212_sl3_12
たしかに、 ありません。
このファイルの末尾に、 InternalsVisibleTo 属性を追加します。

[assembly:InternalsVisibleTo("TestProject1")]

さて、 もう一度、 テスト コードに戻って、 page. と打つと…
20091212_sl3_13a
ちゃんとインテリセンスに FormatDateTime() も出てきました。 次のように、 テストメソッドを完結させます。

[TestMethod()]
public void FormatDateTimeTest01_AM() {
    MainPage page = new MainPage();
    Assert.AreEqual<string>(
        "今は 2009年 12月 12日の午前 6時 54分 32秒です。",
        page.FormatDateTime(new DateTime(2009, 12, 12, 6, 54, 32))
        );
}

ビルドしてみると、 エラーが出ます。
20091212_sl3_15
エラー メッセージから、 System.Windows アセンブリへの参照が必要らしいと分かります。
そこでアセンブリへの参照を追加することになるわけですが、 ここで必要なのは Silverlight 用のアセンブリです。

「参照の追加」 ダイアログで、 [.NET] のほうではなく、 [参照] のタブを使って、 Silverlight 用のリファレンス アセンブリ (参照アセンブリ) を指定します。 (アセンブリ本体を参照しても良いと思いますが、 確認していません。)
※ リファレンス アセンブリ (参照アセンブリ) については、 ScottGu 氏のブログ 「マルチターゲットのサポート」 を参照
20091212_sl3_16b

20091212_sl3_17

これでようやく、 ユニット テストが動きました。
20091212_sl3_18
SL3App01 側でエラーになっていることが確認できます。

【 3. GUI のインスタンス化は出来ない 】

上のテスト結果を見ると、 Silverlight の GUI クラス MainPage のコンストラクター呼び出しの中で、 InvalidClrossThreadAccess だという UnauthorizedAccessException 例外が出ています。 さすがに、 ユニットテストから Silverlight の GUI にアクセスすることは出来ないようです。


【 4. GUI 中のスタティック メンバーはテストできる 】

では、 インスタンス不要のスタティックなものならどうでしょう?
テストコードを、 スタティック メソッドの呼び出しに変更します。

[TestMethod()]
public void FormatDateTimeTest01_AM() {
    //MainPage page = new MainPage();
    Assert.AreEqual<string>(
        "今は 2009年 12月 12日の午前 6時 54分 32秒です。",
        MainPage.FormatDateTime(new DateTime(2009, 12, 12, 6, 54, 32))
        );
}

実装も、 シグネチャだけ修正して、 テストを走らせてみます。
20091212_sl3_20
今度は、 予想通りの失敗になりました。
ここまで来ればしめたものです。 あとは普段どおりに、 最低限の実装を追加してテストを通し、 FormatDateTimeTest01_PM() テストを追加してレッドを確認し、 それに通るだけの実装修正をやって…
20091212_sl3_21
オール グリーンにします。
※ 日付と時刻のテストパターンに注目。 月・日・時・分・秒のそれぞれで、 2桁と 1桁の場合のテストをやっています。 これだけのテストで、 年が 4桁の間はたぶん大丈夫でしょう。

これでロジックは完成しましたので、 実際に GUI を動かして見てみましょう。
20091212_sl3_22

【 5. テストからのコード生成 】

次は、 本来の TDD の流儀に従って、 テストから先に作り始めてみます。
Visual Studio 2010 で強化された 「Generate From Usage (使用法から生成)」 機能は上手く働くでしょうか?

先ほど作ったロジックは Silverlight の UI (MainPage クラス) の中に埋め込んだため、 インスタンス化出来ませんでした。 ロジックを別のクラスとして独立させてあげれば、 きっとインスタンスも作れるでしょう。
表示するメッセージ文字列を提供してくれる MessageProvider というクラスを、 Silverlight のプロジェクトに追加してみましょう。

テストのほうから先に作ります。
テストコードに、 (まだ存在しない) MessageProvider クラスをインスタンス化するコードを書きます。

SL3App01.MessageProvider mp = new SL3App01.MessageProvider();

当然ながら、 「そんなものは無い!」 とばかりに、クラス名の部分に赤い波線が表示されます。

20091212_sl3_23a
そこで、 クラス名のところで右クリックして、[生成] - [クラス] を選びます。 するとクラスのスケルトンが自動生成されて、 赤波線は消えます。
上手くいったように思えますが、 生成されたクラスを探してみると…
20091212_sl3_24
残念、 テストプロジェクトの中に作られてしまっています。
あらためて手作業で MessageProvider クラスを作ります。
※ (2010/1/7 追記) [生成] - [クラス] で同じプロジェクト内に作られるのは仕様です。 別プロジェクトに作りたいときは、 [生成] - [New type...] を選んでプロジェクトを指定しなければなりません。 ⇒ [コラム] [VS2010] 新機能 Generate From Usage (使用法から生成) の使い方 Step by Step

次は、 メソッドです。
テスト コードに、 (まだ実装していない) MessageProvider.FormatDateTime() メソッドの呼び出しをテストするコードを追加します。

SL3App01.MessageProvider mp = new SL3App01.MessageProvider();
Assert.AreEqual<string>(
    "ただいま 2009年 12月 12日の午前 6時 54分 32秒です。",
    mp.FormatDateTime(new DateTime(2009, 12, 12, 6, 54, 32))
    );

「そんなメソッドは無い!」 と怒られますので、 FormatDateTime のところを右クリックして、 [生成] -[メソッドスタブ] を選びます。
20091212_sl3_25a
ちなみに、 スタブを自動生成させても、 エディタのカーソル位置は変わらずにテストコードにあります。 テストを考えている作業を邪魔されることはありません。
こんどは正しくメソッド スタブが生成されたようです。
20091212_sl3_26

テストを実行して、 レッドを確認します。
20091212_sl3_27
予定通り、 NotImplementedException が出ました。
あとは、 いつも通りにテストファーストで作っていきます。

【 出来上がったテストケース 】

//MessageProviderTest

[TestMethod()]
public void FormatDateTimeTest01_AM()
{
    SL3App01.MessageProvider mp = new SL3App01.MessageProvider();
    Assert.AreEqual<string>(
        "ただいま 2009年 12月 12日の午前 6時 54分 32秒です。",
        mp.FormatDateTime(new DateTime(2009, 12, 12, 6, 54, 32))
        );
}

[TestMethod()]
public void FormatDateTimeTest01_PM()
{
    SL3App01.MessageProvider mp = new SL3App01.MessageProvider();
    Assert.AreEqual<string>(
        "ただいま 2009年 9月 7日の午後 10時 0分 2秒です。",
        mp.FormatDateTime(new DateTime(2009, 9, 7, 22, 0, 2))
        );
}

【 出来上がったロジック クラス 】

public class MessageProvider
{
    public string Now {
        get {
            return FormatDateTime(DateTime.Now);
        }
    }

    internal string FormatDateTime(DateTime dt)
    {
        string s = dt.ToString("ただいま yyyy年 M月 d日のtt h時 m分 s秒です。");
        return s;
    }
}

※ GUI から利用する Now プロパティは、 テストファーストしていません。 ご覧の通り、 中で DateTime.Now を使っているため、 テストしにくいからです。

GUI のボタンクリックで、 MessageProvider の Now プロパティを取得して表示させるようにしてみましょう。
20091212_sl3_28

【 同じバイナリなのか? 】

Silverlight プロジェクトでビルドしたバイナリと、 テストプロジェクトでテストしているバイナリは、 ほんとに同一のものでしょうか? テストプロジェクトをビルドしたときに、 通常の .NET Framework 用としてビルドし直されていたりしないのでしょうか?

Silverlight プロジェクトの bin\debug と、 テストプロジェクトの bin\debug の中身を、 WinDiff してみました。
20091212_sl3_29
テスト対象となった sl3app01.dll は、 Silverlight プロジェクトで作られたアセンブリと、 確かに "identical" (同一) であることが確認できました。

【 コード カバレッジ 】

コード カバレッジも、 ちゃんと取得できます。
20091212_sl3_30

【 まとめ 】

ごく簡単なものでしたが、 Silverlight 3 のロジック部分を TDD で作ることは可能だと分かりました。 ただし、 Visual Studio のサポートは不完全であり、 改善されることが望まれます。

|

« [コラム] Silverlight 2 / 3 でも、ユニットテスト出来る | トップページ | [NEWS] NUnit 2.5.3 リリース、 .NET 4.0 に対応 »

*コラム」カテゴリの記事

<MSTest>」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)




トラックバック


この記事へのトラックバック一覧です: [コラム] VS2010b2J (MSTest) で、 Silverlight 3 のロジックを TDD する:

« [コラム] Silverlight 2 / 3 でも、ユニットテスト出来る | トップページ | [NEWS] NUnit 2.5.3 リリース、 .NET 4.0 に対応 »