2010年4月4日日曜日

第31回 テストを作る

今更ですがUnitTestを書きます。
ちゃんとした開発ではもっと早くからやるべきことですが、
たぶんblog的にはつまんないテーマなので後回しにしてました。

知っている人は今回全部飛ばしていいような内容ですが、
一応知らない人のために能書きを書いておきます。
知っている部分はチラ読みで飛ばしてください。

UnitTestとは、実際にソフトを動かしてテストするんじゃなくて、
個々のクラスなどの小さな単位用にテスト用のプログラムを書いて、
用意されたテスト用のフレームワークで、(多くの場合は自動で)動かすものです。
UnitTestの用語としてのちゃんとした定義は違うかもしれませんが、
ここではそいうったテストを指すイメージで読んでいってください。
自動化できるので、ソースコードを変更するたびに実行します。

最近よく見るTDD(Test Driven Developmentだったかな?)は
このクラス(メソッド)はこういう呼び出し方したらこういう動作(戻り値など)をするんですよーという
テストを先に書いてから、実際のメソッドの中身のソースコードを書いていきます。

私は他のアプリ使うときにTDDっぽくやってみて、メリットは以下のようなことだと思っています。

・ソースコードを作る側ではなくて、クラスを使う(呼び出す)側の視点で
 クラス/メソッドを設計することになるので、使いやすいクラスができやすい。
・いつでも手軽に実行できるテストコードが残るので、ソースコードを書き換えやすい。
(意図しない副作用が出たら検出しやすいので、安心して書き換えできる)
・第3者にとって、テストコードがクラスの使い方の見本となる
(ある程度大きな規模になるとクラスがどのように動いているのかわかりにくくなるが、
UnitTestのコードはテスト対象とそれを動かすのに最小限のクラスのみが書かれるので、
どうやって使っていいかがわかりやすい)

意外と1個目のメリットが想像以上に大きいかなと思っています。

実際にWTwitterへの適用ですが、
すでにソースコードを結構書いているので、その部分はTDDになりませんが、
これから新しく書くところはできるだけ先にテストコードを書いていきたいと思います。
また既存の部分へのテストは、いっぺんに書くのはやる気が起きないので、
徐々に書いていきます。

あと、カバレッジ(ソースコードのどれだけの割合をテストで実行したか)は100%にすべきだとか、
全クラス/メソッドごとに書くべきだとか
いろいろ人によって意見はあると思いますが、
ぶっちゃけ「twitterクライアントなんて落ちたら再起動すればいいやん」、くらいにしか思ってないので、
費用対効果が大きそうなところしか書きません。
人命に関わるとかエラーの損害が大きいとか品質が大事なソフトではまた変わってくると思います。

で、WTwitterではMbUnit(Gallio)を使ってみます。
比較的新しいテストフレームワークで実は私は使ったことがないのですが、
よそで便利だと評判だったのと、新しい物好きなので使ってみたかったからです。
.net用のフレームワークではnUnitが一番メジャーかなと思います。
VisualStudioの上位版だったら付属しているUnitTestのフレームワークを使うのもいいかもしれません。
というわけで、最初のうちはMbUnitの利点を生かせないテストになっているかもしれませんが、
それを念頭に置いて以降を読んでください。

準備は、まずGallioをインストールしてください
http://www.gallio.org/

そしてVisualStudioでテスト用の新しいプロジェクト(クラスライブラリ)を作成。
テストプロジェクトにDLL参照の追加(WTwitter, MbUnit, Gallio)

WTwitter本体の方のプロジェクトに戻ってテスト対象をpublicにしてください。
別プロジェクトになるとpublic じゃないとテストできないので注意してください
 (internalのままでテストプロジェクトから見えるようにする方法はあります。)

テストプロジェクトにテスト用のクラスを作成。
テストメソッドには[Test]の属性を付けます。
※nUnitにはクラスに[TestFixture]の属性が必要だったと思いますが、
MbUnitでは必要なくなったのかな?
なくても動くようです。



namespace WTwitter.Tests.Model.Twitter {
 class TwitterItemTest {
  private static class TestDataFactory  {
   public static  Status CreateStatus() {
    return new Status() {
     Id = 100,
     Favorited = false,
     CreatedAtString = "Mon Jan 11 15:26:14 +0000 2010",
     Text = "おはようございます",
     InReplyToScreenName = "yuki",
     InReplyToStatusId = null,
     InReplyToUserId = null,
     Truncated = false,
     User = new User() {
      Id = 33,
      Following = false,
      Name = "yuki_",
      ScreenName = "yuki",
      Url = "http://wtwitter.codeplex.com/",
      ProfileImageUrl = "http://someimage",
      Description = "description for user",
     }
    };
   }
  }

  [Test]
  public void TestConstructor() {
   var status = TestDataFactory.CreateStatus();
   var target = new TwitterItem(status);
   Assert.AreEqual(100,target.Id);
   Assert.IsFalse(target.Favorited);
   var date = new DateTime(2010, 1, 11, 15, 26, 14, DateTimeKind.Utc);
   Assert.AreEqual(date.ToLocalTime(), target.CreatedAt, "CreatedAtはLocalTimeで持つ");
   Assert.AreEqual(ItemType.TwitterStatus, target.Type);
   Assert.AreEqual("おはようございます", target.Text);
   Assert.AreEqual("yuki", target.User.ScreenName);
   Assert.AreEqual("yuki_", target.User.Name );
   Assert.AreEqual("description for user", target.User.Description );
   
  }

  [Test]
  public void TestEquality() {
   //EqualsはIdで判断する

   var status1 = TestDataFactory.CreateStatus();
   var status2 = TestDataFactory.CreateStatus();
   var status3 = TestDataFactory.CreateStatus();
   status1.Id = 10;
   status2.Id = 10;
   status3.Id = 20;
   var item1 = new TwitterItem(status1);
   var item2 = new TwitterItem(status2);
   var item3 = new TwitterItem(status3);

   Assert.IsTrue(item1.Equals(item2));
   Assert.IsFalse(item1.Equals(item3));
  }

  [Test]
  public void TestTextComponent() {
   //細かい動作はSplitterクラスのテストで確認する

   var status1 = TestDataFactory.CreateStatus();
   var item1 = new TwitterItem(status1);
   Assert.AreEqual(1, item1.TextComponents.Count);

   var status2 = TestDataFactory.CreateStatus();
   status2.Text = "@yuki1090 てすと http://www.google.com てすと";
   var item2 = new TwitterItem(status2);
   
   //※"@" "yuki1090" " てすと " "http://www.google.com" " てすと"に分かれる
   //IDに@は含まれないので注意
   Assert.AreEqual(5, item2.TextComponents.Count);
   Assert.AreEqual(TextComponentType.Plain, item2.TextComponents[0].Type);
   Assert.AreEqual(TextComponentType.UserName, item2.TextComponents[1].Type);
   Assert.AreEqual(TextComponentType.Plain, item2.TextComponents[2].Type);
   Assert.AreEqual(TextComponentType.Url, item2.TextComponents[3].Type);
   Assert.AreEqual(TextComponentType.Plain, item2.TextComponents[4].Type);
   Assert.AreEqual("@", item2.TextComponents[0].Text);
   Assert.AreEqual("yuki1090", item2.TextComponents[1].Text);
   Assert.AreEqual(" てすと ", item2.TextComponents[2].Text);
   Assert.AreEqual("http://www.google.com", item2.TextComponents[3].Text);
   Assert.AreEqual(" てすと", item2.TextComponents[4].Text);
  }
 }
}



このテスト対象クラスはStatusクラスのデータをそのまま渡すだけのプロパティが多いので、
コンストラクタのテストと、Equalsのロジックとテキスト分解のロジックをテストしているだけです。
テキスト分解は実際には別クラスがやっているので、
そちらでいろんなパターンのテキストをテストする予定です。
(このクラスのテストではちゃんと呼び出しているかどうかのチェックくらいです)

今回のようにすでにコードを書いている状態であとからテストする場合は、
メソッド名やその上にXmlで書いているコメントのみからテストを書けるかどうか試すといいかと思います。
ソースコードのアルゴリズムを読まないとテストを書けない(使い方や出力がわからない)ような部分は
名称を工夫するなりコメントを増やすなりリファクタリングするなりした方がいいかもしれません。

テストの実行はスタートメニューからIcarus GUI Test Runnerを実行。
テストプロジェクトをビルドして
Icarus(Gallio)に出力されたdllを読み込んで、Start。
全部緑になればOK。

今回のソースコード
http://wtwitter.codeplex.com/SourceControl/changeset/view/44511

0 件のコメント:

コメントを投稿