« TDD って何だっけ? #tddadventjp | トップページ | MSTest‐Windows ストア アプリ開発の暗黒大陸 #win8dev_jp #tddadventjp #tddnet »

2013年12月 8日 (日)

TDD 最初の一歩 (C#編) #tddadventjp

このエントリーは、 TDD Advent Calendar 2013 の 8日目です。
このエントリーのサンプルコードは、 Microsoft Developer Network サンプル  「TDD 最初の一歩 (C#編) 言語: C# Visual Studio 2012 用」 からダウンロードできます。

TDD Advent Calendar を読んでる人の中には 「TDD やったことないよ!」 という開発者もいらっしゃることでしょう。
まずは、 やってみましょう。

本稿では、 無償の Visual Studio Express 2012 を使い、 C# でコーディングします。

 

■ まえふり ~ .NET Framework と TDD

.NET Framework で TDD やるための情報は、 たぶんとても少ないのです。

というのも、 言語を付けて TDD をぐぐってみると、 ↓こんな感じだから。

TDD Java 約 197,000 件
TDD Ruby 約 151,000 件
TDD PHP 約 149,000 件
TDD JavaScript 約 122,000 件
TDD Python 約 101,000 件
TDD C# 約 61,700 件
TDD VB 約 41,600 件

桁違い… orz

本屋を巡っても書籍はありませんし。
あ、 昔は 1冊↓あったんですよ。 ずいぶん前に絶版になってます orz

20131207_tdd01
Microsoft.NET でのテスト駆動開発

まぁ、 そんなわけで、 このサイトをやってたり、 CodeZine で連載させてもらったりしてるわけです。

 

■ 課題 ~ FizzBuzz プログラム

1 から 100 までの Fizz Buzz をコンソールに表示するプログラムを作れ。
実行ファイルの名前は 「FizzBuzz1to100.exe」 とする。

Fizz Buzz (Wikipedia より)

最初のプレイヤーは「1」と数字を発言する。 次のプレイヤーは直前のプレイヤーの次の数字を発言していく。
ただし、 3で割り切れる場合は 「Fizz」、 5で割り切れる場合は 「Buzz」、 両者で割り切れる場合は 「Fizz Buzz」 を数の代わりに発言しなければならない。

 

■ 自動化されたテストをとりあえず考えてみる

TDD の第1規則は 「自動テストが失敗している場合に限り、 新しいコードを書く。 (テストファースト)」 ですから、 まずは自動化されたテストが作れるかを考えてみましょう。

課題はコンソールプログラムですから、 自動化されたテストを作ろうと思えばなんとかなります。

  1. 自動化されたテストから FizzBuzz1to100.exe を起動する。
  2. FizzBuzz1to100.exe の標準出力を受け取る (または、 標準出力をファイルに落として、 そのファイルを読み取る)。
  3. 出力結果がスペック通りになっているか検証する。

けっこうメンドクサイですね。 ここまで手を掛けて Fizz Buzz 「ごとき」 をテストする価値はあるのでしょうか?

※注: これは受け入れテスト (Acceptance Test) です。 受け入れテストの失敗するテストケースをひとつだけ、自動化されたテストとして実装し、そのテストに合格するように製品コードを実装し、 また次の失敗するテストケースを… というように進めていけば TDD になります。 が、 一度に実装すべき製品コードの粒度が大きいので、 受け入れテストだけで TDD するのは困難です。 通常は、 もっと小さな粒度で TDD します。 なお、 ATDD (Acceptance Test-Driven Development) は一般に、 自動化を必須とはしませんし、 事前にテストケースを全部作ります。

 

■ プログラムの構造を考えてみる

FizzBuzz1to100.exe の全体は、 こんなコードになるはずですね。

void main()
{
  for (n が 1 から 100 まで)
  {
    nに対するFizzBuzz文字列を算出する;
    結果を出力する;
  }
}

この全てを自動化されたテストで保証する必要があるでしょうか? (もちろん、 そういう 「世界」 もあるでしょう)

Fizz Buzz 「ごとき」 なら、 次の部分は最後に手動テストで確認するだけでも良いでしょう。
・for ループ (1から始まり、100で終わる)
・結果を出力する (コンソールに実際に文字が出てくる)

なぜなら、 これらの部分にはそれほどバグが潜んでいるとは思えず、 また、 さほど変更されるとも思えないからです。
※ 変更がありそうなのは 「100 まで」 ですが、 これは出力される行数をチェックするだけですね。 例えばこんなふうにして。
FizzBuzz1to100.exe | wc -l

そして、 前述したように、 それらの部分をテストするための受け入れテストを自動化するのはメンドクサイです。 コストパフォーマンスを考えると、 Fizz Buzz 「ごとき」 では、 これらを TDD する価値は無いです。

対して、 「nに対するFizzBuzz文字列を算出する」 という部分はどうでしょう。
ここは FizzBuzz プログラムの心臓部ですね。 そしてここには、 バグが入り込みやすそうです。 また、 自動化されたテストを書くのは、 標準出力とかを取り扱わなくて済みますから、 わりと簡単にできそうです。

この心臓部だけを取り出して TDD するのは、 じゅうぶんに価値がありそうです。

 

■ FizzBuzz の心臓部を TDD する

では、 「nに対するFizzBuzz文字列を算出する」 部分を TDD していきましょう。
Visual Studio Express 2012 for Windows Desktop を使います。

この部分をひとつのメソッドに切り出すことにします。 メソッド名は GetFizzBuzz としましょうか。

ここで大抵は、 これから作るメソッドのスペック (メソッドに与える引数と、 それに対するメソッドの振る舞い) を開発者が決定しなければならない (あるいは、 TDD しながら順次決めていく) のですが、 今回は FizzBuzz の定義がほぼそのまま使えます。
なので、 いきなり TDD を始めることができます。

 

◇ テストを書く準備をする

まず、 テストプロジェクトを作ります。

プロジェクト テンプレート: 単体テスト プロジェクト
プロジェクト名: FizzBuzzTest
ソリューション名: FizzBuzz

20131207_tdd02

自動生成された UnitTest1.cs を次の名前にリネームします。

クラス名: FizzBuzzTest.cs

 

◇ 1つめのテスト

さぁ、 テストファーストを始めましょう。

最初の失敗するテストは何にしましょうか? 簡単に実装できるスペックがいいです。
いきなり 『両者で割り切れる場合は 「Fizz Buzz」』 の自動化されたテストから始めてしまうと、 実装がメンドクサイので挫折します。
『最初のプレイヤーは「1」と数字を発言する』から取り掛かりましょう。

自動生成されたメソッド TestMethod1 を次の名前にリネームします。

最初のテスト: GetFizzBuzzTest01_最初のプレイヤーは1と数字を発言する

このテストは、 GetFizzBuzz メソッドに int の 1 を与えたら、 string で "1" が返ってくれば OK ですね。
次のように自動化されたテストを書きます。

【テストケース】最初のプレイヤーは1と数字を発言する (未完成)
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace FizzBuzzTest
{
  [TestClass]
  public class FizzBuzzTest
  {
    [TestMethod]
    public void GetFizzBuzzTest01_最初のプレイヤーは1と数字を発言する()
    {
      Assert.AreEqual<string>("1", GetFizzBuzz(1));
    }
  }
}

これはコンパイルできません (=自動テストが失敗している、Red)。

テストがひとつ Red になったので、 それを Green にする分だけ製品コードを書けます。

ソリューションに製品コードのプロジェクトを追加します。

プロジェクト テンプレート: コンソール アプリケーション
プロジェクト名: FizzBuzz1to100

作った FizzBuzz1to100 プロジェクトにクラスを追加します。

クラス名: FizzBuzz

自動生成された FizzBuzz クラスのスコープは internal なので、 public に変更します

class FizzBuzzpublic class FizzBuzz

FizzBuzzTest プロジェクトに戻り、 その参照設定に FizzBuzz1to100 を追加します。
20131207_tdd03
これで、 テストから製品コードが見えるようになりました。

テストコードに、 製品コードの名前空間とクラス名を追加します。

【テストケース】最初のプレイヤーは1と数字を発言する (完成)
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using FizzBuzz1to100;

namespace FizzBuzzTest
{
  [TestClass]
  public class FizzBuzzTest
  {
    [TestMethod]
    public void GetFizzBuzzTest01_最初のプレイヤーは1と数字を発言する()
    {
      Assert.AreEqual<string>("1", FizzBuzz.GetFizzBuzz(1));
    }
  }
}

まだコンパイルできません (Red)。
FizzBuzz1to100 名前空間に FizzBuzz クラスは作りましたが、 GetFizzBuzz メソッドはまだ存在しないからですね。

上のテストコードの GetFizzBuzz にカーソルを置いて右クリックし、[生成] - [メソッド スタブ] を選んで、 メソッドのスケルトンを自動生成させます。

これでコンパイルが通るようになりました。

さて、 Red が無いと製品コードの実装を進められません。
最初のテストケースがコンパイルできるようになったのですから、 テストを実行してみましょう。

テストエクスプローラー (メニュー [テスト] - [ウィンドウ] - [テスト エクスプローラー]) で 「すべて実行」 リンクをクリックすると、 テストが自動検出されて実行されます。

20131207_tdd04

Red になりました!
この Red を解消する分だけ、 製品コードを書き進められます。

Green にするには、 「テストを速やかに動くようにする。その過程で必要ならどんな罪をも犯す」。
ですから、 文字列定数 "1" を返しちゃいましょう。

【製品コード】Green にするため、文字列定数 "1" を返す
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace FizzBuzz1to100
{
  public class FizzBuzz
  {
    public static string GetFizzBuzz(int p)
    {
      //throw new NotImplementedException();
      return "1";
    }
  }
}

ふたたびテストを「すべて実行」

20131207_tdd05

Green になりました♪

Green になったので、 一息入れて、 製品コードを眺めてみます。
次のリファクタリングをやっておきましょう。

・引数名 p を n に
・とりあえずコメントとして残しておいた自動生成されたコードを削除

【製品コード】ちょっとリファクタリング
public class FizzBuzz
{
  public static string GetFizzBuzz(int n)
  {
    return "1";
  }
}

製品コードを書き換えたので、 ふたたびテストを「すべて実行」 して、 壊していないことを確認しておきます。

Red → Green → Refactor の最初のサイクルが終わりました。
このように、 TDD の初めは、 Red に持ち込むまでが面倒です。 Red → Green → Refactor のサイクルが回り始めたら、 どんどん進んでいけます。

 

◇ 2つめのテスト

それでは、 2つめのテストケースに進みましょう。
すなおに、 「次のプレイヤーは直前のプレイヤーの次の数字を発言していく。」 をやりましょうか。 といっても 「次の」 では具体的に書けませんから、 2番目のとしましょう。
次のメソッドを追加します。

【テストケース】2番目のプレイヤーは1の次の数字を発言する
[TestMethod]
public void GetFizzBuzzTest02_2番目のプレイヤーは1の次の数字を発言する()
{
  Assert.AreEqual<string>("2", FizzBuzz.GetFizzBuzz(2));
}

「すべて実行」でテストを実行し、 Red になることを確認します。

Visual Studio は次のように報告しています。

結果  のメッセージ:    Assert.AreEqual に失敗しました。<2> が必要ですが、<1> が指定されました。

製品コードは定数文字列 "1" を返すように書いてあるので、 「<1> が指定されました」 となるのは当然ですね。

Red になったので、 それを Green にするためだけの修正を製品コードに加えます。 引数 n を ToString() して返すようにすれば良いですね。

【製品コード】引数 n を ToString() して返す
public class FizzBuzz
{
  public static string GetFizzBuzz(int n)
  {
    //return "1";
    return n.ToString();
  }
}

ふたたびテストを「すべて実行」、Green になります♪

一息ついて、 コードを眺めてみます。
製品コードは、 要らなくなったコードを削除するくらいですね。
テストコードは、 重複が気になります。 ほぼ同じ形のテストメソッドが 2つ並んでいますから。 しかし、 これは Visual Studio 標準のテストフレームワーク (MSTest) では、 いかんともしがたいです。 NUnit ならば、 TestCase 属性を使って、 きれいにまとめられるんですけどね。 ということで、 ここは目をつぶりましょうw

 

◇ 3つめのテスト

では、 3つめのテストケースに進みます。 『ただし、3で割り切れる場合は 「Fizz」』です。
「3で割り切れる場合」も無限にありますから、 3のときにしましょう。

次のメソッドを追加します。

【テストケース】3番目のプレイヤーはFizzと発言する
[TestMethod]
public void GetFizzBuzzTest03_3番目のプレイヤーはFizzと発言する()
{
  Assert.AreEqual<string>("Fizz", FizzBuzz.GetFizzBuzz(3));
}

「すべて実行」でテストを実行し、 Red になることを確認します。
そうしたら、 Green にするためには…?
引数 n の 3 の剰余が 0 だったら "Fizz" を返すようにすればよさそうです。

【製品コード】引数 n の 3 の剰余が 0 だったら "Fizz" を返す
public static string GetFizzBuzz(int n)
{
  if (n % 3 == 0)
    return "Fizz";

  return n.ToString();
}

ふたたびテストを「すべて実行」、Green になります♪

ちょっと一息ついてコードを眺めてみますが、 重複は見当たらないし、
いい加減な名前やコードもなさそうです。
※ "Fizz" を文字列定数に置き換えるべきかどうか、 という議論はあるでしょう。 私は、 一ヵ所にしか登場しない文字列は、 そのままにしておくことが多いです (i18nしない場合)。

ところで、 「3で割り切れる場合」 というスペックに対して、 3 しかテストしていませんが、 大丈夫でしょうか?
例えば 6 の場合は? 製品コードを追いかけてみると、 大丈夫そうです。
もしも不安なら、 「6番目のプレイヤーはFizzと発言する」 テストを書いてみます。
それで Red になれば、 製品コードを修正すればいいです。
Red にならなければ、 そのテストは TDD には不要なテストですから、 削除します。
※ 不要なテストを抱え込むと、 それにはドキュメントとしての意味はあるでしょうが、 しかし、 仕様変更 (=テストケースの変更) のときの負担が増えてしまいます。

 

◇ 4つめのテスト

『5で割り切れる場合は 「Buzz」』 ですから、 5 のときをやりましょう。

【テストケース】5番目のプレイヤーはBuzzと発言する
[TestMethod]
public void GetFizzBuzzTest04_5番目のプレイヤーはBuzzと発言する()
{
  Assert.AreEqual<string>("Buzz", FizzBuzz.GetFizzBuzz(5));
}

Red を確認したら、 3 のときと同様に製品コードを修正します。

【製品コード】引数 n の 5 の剰余が 0 だったら "Buzz" を返す
public static string GetFizzBuzz(int n)
{
  if (n % 3 == 0)
    return "Fizz";

  if (n % 5 == 0)
    return "Buzz";

  return n.ToString();
}

Green を確認したら、 リファクタリングすべきところはないか、 コードを眺めてみます。
このままでよさそうです。

 

◇ 5つめのテスト

スペックは 「両者で割り切れる場合は 「Fizz Buzz」」 となっています。

「両者で割り切れる」 というのは、 3 でも 5 でも割り切れるという意味ですね。
ということは、 3 と 5 の最小公倍数である 15 をやればよさそうです。

【テストケース】15番目のプレイヤーは「Fizz Buzz」と発言する
[TestMethod]
public void GetFizzBuzzTest05_15番目のプレイヤーはFizz_Buzzと発言する()
{
  Assert.AreEqual<string>("Fizz Buzz", FizzBuzz.GetFizzBuzz(15));
}

Red を確認したら、 製品コードを修正します。

【製品コード】引数 n の 3 の剰余が 0 かつ 5 の剰余が 0 だったら "Fizz Buzz" を返す
public static string GetFizzBuzz(int n)
{
  if (n % 3 == 0 && n % 5 == 0)
    return "Fizz Buzz";

  if (n % 3 == 0)
    return "Fizz";

  if (n % 5 == 0)
    return "Buzz";

  return n.ToString();
}

Green になったので、 重複を探してみます。
n % 3 == 0 が 2回、 n % 5 == 0 も 2回出現していますね。
この重複は、 変数にキャッシュしちゃいましょう。 説明用の変数として is3の倍数is5の倍数 を導入します。

【製品コード】剰余計算を変数にキャッシュ
public static string GetFizzBuzz(int n)
{
  bool is3の倍数 = (n % 3 == 0);
  bool is5の倍数 = (n % 5 == 0);

  if (is3の倍数 && is5の倍数)
    return "Fizz Buzz";

  if (is3の倍数)
    return "Fizz";

  if (is5の倍数)
    return "Buzz";

  return n.ToString();
}

最後にテストを 「すべて実行」 して、 製品コードを壊していないことを確認します。

 

◇ TDD の終了

もう見落としはないでしょうか?
ほかに失敗するテストケースはないでしょうか?
リファクタリングすべきところは残っていないでしょうか?

それらがもう見つからないようなら、 TDD は終了です。

 

■ プログラムを完成させる

TDD で作成した GetFizzBuzz メソッドを Main 関数に組み込んで、 プログラムを完成させましょう。

自動生成された Program クラスの Main メソッドを、 次のように書き換えます。

【製品コード】Main 関数 (TDD していない!)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace FizzBuzz1to100
{
  class Program
  {
    static void Main(string[] args)
    {
      for (int n = 1; n <= 100; n++)
        Console.WriteLine(FizzBuzz.GetFizzBuzz(n));

#if DEBUG
      //こうしないとデバッグ実行用コンソールが閉じてしまう
      Console.ReadKey();
#endif
    }
  }
}

以上で完成です。
余力があれば、 完成した FizzBuzz1to100.exe を自動テストする仕掛けを作ってみてください。
TDD しなかった部分というのは、 事実上 「for (int n = 1; n <= 100; n++)」 だけです。 そこをテストするためだけに見合う労力で作れるでしょうか?

 

■ まとめ

FizzBuzz プログラムの一部分を TDD で開発した。

  • TDD は、 最小限のテストケースで製品コードを生み出す (TDD の目的は「Clean code that works」。品質保証のためにテストケースを網羅するのとは目的が違う)
  • 製品コード全体を TDD するとは限らない (むしろ、 コストパフォーマンスが見合わない部分は TDD しない)
  • 開発の最初に、 TDD するパーツと TDD しないパーツに分ける (じつはここが一番難しいのかもしれない)

 

20131207_tdd06

|

« TDD って何だっけ? #tddadventjp | トップページ | MSTest‐Windows ストア アプリ開発の暗黒大陸 #win8dev_jp #tddadventjp #tddnet »

*TDD の練習」カテゴリの記事

コメント

コメントを書く



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




トラックバック

この記事のトラックバックURL:
http://app.cocolog-nifty.com/t/trackback/209349/58713554

この記事へのトラックバック一覧です: TDD 最初の一歩 (C#編) #tddadventjp:

« TDD って何だっけ? #tddadventjp | トップページ | MSTest‐Windows ストア アプリ開発の暗黒大陸 #win8dev_jp #tddadventjp #tddnet »