最近Javaを使う仕事をしていたので、何年かぶりにxUnitを使ったテストを書いていたのですが、やっぱり仕様をテストコード上に直接表現できるSpecの方が私は好きです。
で、ちょっと調べたらVisualStudioのアドインでSpecFlowなるものがあることを発見。で、ついでにMonoDevelop版もあることを発見。肝心のJavaの方は、なんかScalaを使ったものはあるようですが、まだまだjUnitが主流のようです。まあ、Javaの世界だからこれは仕方ない。

というわけで今回はMonoDevelopを使ってC#でSpecテストを書いてみた。

MonoDevelopの Tool -> Add-in Manager からSpecFlow Supportをインストール。
27)

次にSpecFlowのサイトから、というかGitHubからdllをダウンロード。
http://specflow.org/home.aspx
※Windows版はmsiインストーラ版が用意されていますが、それ以外の場合はZIPファイルをダウンロード&解凍して使う必要があります。

足し算をするクラスのテストをSpecFlowを使って書いてみる。
まず、NunitLibraryProjectをMySpecという名前で新規作成してみる。というのもSpecFlowは内部でNUnitを使っているので、デフォルトでNunitのライブラリを参照するように設定してくれるNunitLibraryProjectがちょっと楽だから。
23)

次にライブラリの参照でダウンロード&解凍したTechTalk.SpecFlow.dllを追加。
01)

次に、ファイルの新規作成で SpecFlowFeatureを追加。ここではCalculator.featureという名前で作ってみる。
32)

サンプルとしてこんな感じのモノが書かれている。

		Feature: Addition
			In order to avoid silly mistakes
			As a math idiot
			I want to be told the sum of two numbers

		@mytag
		Scenario: Add two numbers
			Given I have entered 50 into the calculator
			And I have entered 70 into the calculator
			When I press add
			Then the result should be 120 on the screen
	
先頭のFeature: の部分はタイトルとフリーテキストが入力可能で、まあ、こんな機能のテストをしますよー的なことを書いとけばいいんだと思います。
で、Scenarioってのがそれぞれのテストケースを書くところのよう。多分意味は
  • Given : 事前の条件的なこと(〜の場合に)
  • When : テスト対象の操作的なこと(〜をしたら)
  • Then : 結果(~になるはず)
てな感じだと思います。
この場合だと、50と70を入力して、addを押したら、結果は120を表示されるはず、てなテストになります。

とりあえずこの状態でテストを実行してみると結果はこんな感じになる。
44)

		Running MySpec.MySpec.MySpec.AdditionFeature.AddTwoNumbers ...
		Given I have entered 50 into the calculator
		-> No matching step definition found for the step. Use the following code to create one:
		    [Binding]
		    public class StepDefinitions
		    {
		        [Given(@"I have entered 50 into the calculator")]
		        public void GivenIHaveEntered50IntoTheCalculator()
		        {
		            ScenarioContext.Current.Pending();
		        }
		    }

		And I have entered 70 into the calculator
		-> No matching step definition found for the step. Use the following code to create one:
		    [Binding]
		    public class StepDefinitions
		    {
		        [Given(@"I have entered 70 into the calculator")]
		        public void GivenIHaveEntered70IntoTheCalculator()
		        {
		            ScenarioContext.Current.Pending();
		        }
		    }

		When I press add
		-> No matching step definition found for the step. Use the following code to create one:
		    [Binding]
		    public class StepDefinitions
		    {
		        [When(@"I press add")]
		        public void WhenIPressAdd()
		        {
		            ScenarioContext.Current.Pending();
		        }
		    }

		Then the result should be 120 on the screen
		-> No matching step definition found for the step. Use the following code to create one:
		    [Binding]
		    public class StepDefinitions
		    {
		        [Then(@"the result should be 120 on the screen")]
		        public void ThenTheResultShouldBe120OnTheScreen()
		        {
		            ScenarioContext.Current.Pending();
		        }
		    }
	
ステップ定義とかいうものが必要になってくるらしい。というわけでステップ定義とやらを追加してみる。

ファイルの新規作成で、SpecFlow Spec Definitionを選択して適当にCalculatorStepという名前で作ってみると、サンプルコード入りのファイルが作成されるので、とりあえずサンプルのメソッドは全て削除。するとこんな感じ。 58)

		using System;

		using TechTalk.SpecFlow;

		namespace MySpec
		{
			[Binding]
			public class CalculatorStep
			{
			}
		}
	

で、ここで上記テストの実行結果にある定義すべきメソッドをコピペして貼り付ける。するとこんな感じになる。

		using System;
		using TechTalk.SpecFlow;

		namespace MySpec
		{
			[Binding]
			public class CalculatorStep
			{
				[Given(@"I have entered 50 into the calculator")]
				public void GivenIHaveEntered50IntoTheCalculator ()
				{
					ScenarioContext.Current.Pending ();
				}

				[Given(@"I have entered 70 into the calculator")]
				public void GivenIHaveEntered70IntoTheCalculator ()
				{
					ScenarioContext.Current.Pending ();
				}

				[When(@"I press add")]
				public void WhenIPressAdd ()
				{
					ScenarioContext.Current.Pending ();
				}

				[Then(@"the result should be 120 on the screen")]
				public void ThenTheResultShouldBe120OnTheScreen ()
				{
					ScenarioContext.Current.Pending ();
				}
			}
		}

	
これでテストを実行するとさっきとは違ったメッセージになる。
		Running MySpec.MySpec.MySpec.AdditionFeature.AddTwoNumbers ...
		Given I have entered 50 into the calculator
		-> pending: CalculatorStep.GivenIHaveEntered50IntoTheCalculator()
		And I have entered 70 into the calculator
		-> skipped because of previous errors
		When I press add
		-> skipped because of previous errors
		Then the result should be 120 on the screen
		-> skipped because of previous errors
	

まだ実際に動くCalculatorクラスなんてものはないけど、このシナリオ通りにテストコードを書いてみる。この場合、まだ物がないので、完全に呼び出す側の立場でメソッド名とかを考えられる、、、。

		using System;
		using NUnit.Framework;
		using TechTalk.SpecFlow;

		namespace MySpec
		{
			[Binding]
			public class CalculatorStep
			{

				Calculator target = new Calculator();

				[Given(@"I have entered 50 into the calculator")]
				public void GivenIHaveEntered50IntoTheCalculator ()
				{
					target.Enter (50);
				}

				[Given(@"I have entered 70 into the calculator")]
				public void GivenIHaveEntered70IntoTheCalculator ()
				{
					target.Enter (70);
				}

				[When(@"I press add")]
				public void WhenIPressAdd ()
				{
					target.PressAdd ();
				}

				[Then(@"the result should be 120 on the screen")]
				public void ThenTheResultShouldBe120OnTheScreen ()
				{
					Assert.AreEqual (120, target.Result);
				}
			}
		}
	
この段階では当たり前だけどテストはおろか、コンパイルも通らない。

じゃあ、実際に動くクラス Calculatorを作る。
Add New Projectで、新規プロジェクトを追加。名前は適当にCalcとしてみる。
01)

で、このプロジェクトにCalculatorクラスを追加。
59)

で、必要なメソッドを、プロパティ等のスケルトンを実装すると、こんな感じ。

		using System;
		namespace Calc
		{
			public class Calculator
			{
				public int Result { get; set; }

				public Calculator ()
				{
				}

				public void Enter (int number)
				{
				}

				public void PressAdd ()
				{ 
				}
			}
		}
	

次に、テストプロジェクトの方で、このクラスを参照できるようにライブラリ参照の設定でプロジェクト参照を設定&using句も追加。
07)
18)

		using System;
		using NUnit.Framework;
		using TechTalk.SpecFlow;
		using Calc;
	
これでテストを実行してみると、結果はちゃんと失敗する。
39)

で、今度はテストがちゃんと通るようにCalculatorクラスのメソッドの中身を実装する。とりあえずこんな感じで実装てみる。

		using System;
		using System.Collections.Generic;

		namespace Calc
		{
			public class Calculator
			{

				List _numbers = new List(2);
				public int Result { get; set; }

				public Calculator ()
				{
				}

				public void Enter (int number)
				{
					_numbers.Add (number);
				}

				public void PressAdd ()
				{ 
					_numbers.ForEach( number => this.Result += number);			
				}
			}
		}
	
これで、再度テストを実行してみる。
48)
ちゃんとグリーンで成功!

でもこの感じだと、Enter(50), Enter(70)とかそれぞれにステップ定義の方にメソッドがあって、じゃあ、80を入力した場合のテストを書く場合は、こんな感じで都度メソッドを追加していくのか?、という疑問が、、、。

		//いちいちパラメータ毎にメソッドが必要?
		[Given(@"I have entered 80 into the calculator")]
		public void GivenIHaveEntered80IntoTheCalculator ()
		{
			target.Enter (80);
		}
	
実はその辺もちゃんと用意されていて、正規表現を使ってパラメータ化できたりします。で、featureとステップ定義をこんな感じで書き換えてみる。
feature:
		Feature: Addition
			In order to avoid silly mistakes
			As a math idiot
			I want to be told the sum of two numbers

		@mytag
		Scenario: Add two numbers
			Given I have entered "50" into the calculator
			And I have entered "70" into the calculator
			When I press add
			Then the result should be "120" on the screen
	
ステップ定義:
		using System;
		using NUnit.Framework;
		using TechTalk.SpecFlow;
		using Calc;

		namespace MySpec
		{
			[Binding]
			public class CalculatorStep
			{

				Calculator target = new Calculator();

				//[Given(@"I have entered 50 into the calculator")]
				[Given(@"I have entered ""(\d+)"" into the calculator")]
				public void GivenIHaveEnteredNumberIntoTheCalculator (int number)
				{
					target.Enter (number);
				}

		//		[Given(@"I have entered 70 into the calculator")]
		//		public void GivenIHaveEntered70IntoTheCalculator ()
		//		{
		//			target.Enter (70);
		//		}

				[When(@"I press add")]
				public void WhenIPressAdd ()
				{
					target.PressAdd ();
				}

				//[Then(@"the result should be 120 on the screen")]
				[Then(@"the result should be ""(\d+)"" on the screen")]
				public void ThenTheResultShouldBeOnTheScreen (int result)
				{
					Assert.AreEqual (result, target.Result);
				}
			}
		}
	
featureの方はパラメータ化したい値をダブルクオーテーションで囲みます。
ステップ定義の方は各メソッドのAttributeの中でパラメータ化した部分を正規表現パターンで置き換えます。ここではintが渡されるので数値以外受け付けないようにしてある。

やってみた感じ、やっぱりxUnitよりはSpecの方がいいですね。
また、結果をHTMLで表示したり、ということもできたりするようです。詳しくは下記のサイトとかガイドをご参照ください。
http://specflow.org/home.aspx
http://github.com/downloads/techtalk/SpecFlow/SpecFlow%20Guide.pdf