C#で日本語受入テストの自動化ができるSpecFlowを使おう


SpecFlowは「[Cucumber for .NET](http://www.specflow.org/)」です。   
CucumberはRubyで2009年頃に登場した受入テストのためのテストフレームワークで、現在では様々な言語で同様に利用できるようになっています。
C# でも同様の仕組みがあったので動かし方をまとめました。

# 準備

## プロジェクト作成

SpecFlow用のプロジェクトを作成します。
このとき、クラスライブラリ or 単体テストプロジェクトにしないといけません。
![specflow-create-project.jpg](https://qiita-image-store.s3.amazonaws.com/0/10494/d81ccdc7-b27e-ccd8-2586-0d765ac6bf85.jpeg "specflow-create-project.jpg")

今回は、Specsというプロジェクト名にします。


## SpecFlowのインストール

NuGetパッケージマネージャーコンソールを起動します。
![specflow-install.jpg](https://qiita-image-store.s3.amazonaws.com/0/10494/8e021657-6a72-fe59-09ed-6cc8697a2d68.jpeg "specflow-install.jpg")

以下を入力します。（ここでのSpecsは任意のプロジェクト名）

```
PM> Install-Package SpecFlow -ProjectName Specs
```

### 関連パッケージ

関連するパッケージには以下があります。
必要であれば任意のパッケージを入れます。
今回はMSTestを利用するので特に必要ありません。

* SpecFlow.NUnit

ReSharperなどのテストランナーでSPecFlowやNUnitを動かすためのもの

* SpecFlow.NUnit.Runners

Nunitで[AfterTestRun]をフックしたい場合に利用する（https://github.com/techtalk/SpecFlow/issues/26）

* SpecFlow.xUnit

SpecFlow と xUnit　をインストールする
 
* SpecRun.SpecFlow 

SpecFlow と SpecRun をインストールする

* SpecFlow.CustomPlugin

SpecFlowのプラグインを作成したい場合に利用する

## Visual Studioの拡張

Visual Studioで簡単にSpecFlowを利用できるように入れます。

ツール　→　拡張機能と更新プログラムでSpecFlowを検索して、インストール
![specflow-install2.png](https://qiita-image-store.s3.amazonaws.com/0/10494/31342c5c-50b6-2021-3115-78955dfae8a4.png "specflow-install2.png")


# 設定ファイル

上記の関連パッケージを一つもインストールしなかった場合は自分で設定ファイル（App.config）を書き換える必要があります。

一番単純なのは、以下のように```<unitTestProvider>```を記述することです。

```App.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="specFlow" type="TechTalk.SpecFlow.Configuration.ConfigurationSectionHandler, TechTalk.SpecFlow" />
  </configSections>
  <specFlow>
    <unitTestProvider name="MsTest" />
  </specFlow>
</configuration>
```

```<unitTestProvider>``` に指定する値は以下から選択できます。
http://www.specflow.org/documentation/Unit-Test-Providers/

featureで日本語を利用できるようにするには、以下を追記します。

```
    <language feature="ja-JP" />
    <bindingCulture name="ja-JP" />    
```

specflowはfeatureファイルからテストコードを自動生成しているだけなので、自動生成されるコードに関しての設定があります。


```
  <generator 
      allowDebugGeneratedFiles="false" 
      allowRowTests="true"
      generateAsyncTests="false" />
```

traceに関しては以下で変更できます。

```
  <trace 
      traceSuccessfulSteps="true"
      traceTimings="false"
      minTracedDuration="0:0:0.1"
      stepDefinitionSkeletonStyle="RegexAttribute" />
```

利用する外部のアセンブリ（例：MySharedBindings.dll）がある場合は以下のように指定します。

```
  <stepAssemblies>
    <stepAssembly assembly="MySharedBindings" />
  </stepAssemblies>
```

SpecFlowの動作を変えたい場合の拡張Pluginを指定できます。
この拡張Pluginでは、generator と runtime componentsの動作を変更できます。

```
<specFlow>
  <plugins>
    <add name="MyPlugin" />
  </plugins>
</specFlow>
```

他に使いそうなものに、テストを最初のエラーでやめるかどうかも紹介しておきます。

```
  <runtime 
      stopAtFirstError="false"
      /> 
```

# 必要なアセンブリを取得

SpecFlowが利用するアセンブリをNugetで取得します。

```
PM> Install-Package Microsoft.VisualStudio.QualityTools.UnitTestFramework
```

Specsプロジェクトでアセンブリの参照設定で上記アセンブリを追加します。

# テスト対象のソース

ここではサンプルとしてあるボウリングを表すドメインを使います。

https://github.com/techtalk/SpecFlow-Examples/blob/master/BowlingKata/BowlingKata-MsTest/Bowling/Game.cs

# テストソース

まずは、Stepsファイルを作成します。
日本語を記述するfeaturesファイルだけでは、当然ですが動作しません。
実際のプログラムとfeaturesファイルを繋げるプログラムが必要になります。
それがStepsファイルです。

（Visual Studioの拡張機能を追加しておくこと）
追加　→　新しい項目　→　Specflow Step Definition
![specflow-steps.png](https://qiita-image-store.s3.amazonaws.com/0/10494/9c905ba2-5f06-45b6-1f4e-7b4e47ccaa95.png "specflow-steps.png")

次がコードになります。
これだけだとわからないと思うので、featuresファイルとともに説明します。

```BowlingSteps.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TechTalk.SpecFlow;

namespace Bowling.Specflow
{
    [Binding]
    public class BowlingSteps
    {
        private Game _game;

        [Given(@"新しいゲームを始める")]
        public void GivenANewBowlingGame()
        {
            _game = new Game();
        }

        [When(@"すべてガーター")]
        public void WhenAllOfMyBallsAreLandingInTheGutter()
        {
            for (int i = 0; i < 20; i++)
            {
                _game.Roll(0);
            }
        }

        [When(@"すべてストライク")]
        public void WhenAllOfMyRollsAreStrikes()
        {
            for (int i = 0; i < 12; i++)
            {
                _game.Roll(10);
            }
        }

        [Then(@"次のスコアになります:(\d+)")]
        public void ThenMyTotalScoreShouldBe(int score)
        {
            Assert.AreEqual(score, _game.Score);
        }

        [When(@"(\d+)本倒した")]
        public void WhenIRoll(int pins)
        {
            _game.Roll(pins);
        }

        [When(@"一投目(\d+)本、二投目(\d+)本倒した")]
        public void WhenIRoll(int pins1, int pins2)
        {
            _game.Roll(pins1);
            _game.Roll(pins2);
        }


        [When(@"(\d+)フレームすべてで、一投目(\d+)本、二投目(\d+)本倒した")]
        public void WhenIRollSeveralTimes2(int rollCount, int pins1, int pins2)
        {
            for (int i = 0; i < rollCount; i++)
            {
                _game.Roll(pins1);
                _game.Roll(pins2);
            }
        }

        [When(@"次のピンを倒した:(.*)")]
        public void WhenIRollTheFollowingSeries(string series)
        {
            foreach (var roll in series.Trim().Split(','))
            {
                _game.Roll(int.Parse(roll));
            }
        }

        [When(@"次の本数を倒した")]
        public void WhenIRoll(Table rolls)
        {
            foreach (var row in rolls.Rows)
            {
                _game.Roll(int.Parse(row["本数"]));
            }
        }

    }
}

```

次にfeaturesファイルを作成します。

```ScoreCaluculation.feature
機能: ボウリングスコア計算
  トータススコアを計算するシステムがほしい
  
シナリオ:ガーター 
  前提 新しいゲームを始める
  もし すべてガーター
  ならば 次のスコアになります:0
  
シナリオ: 初心者
  前提 新しいゲームを始める
  もし 一投目2本、二投目7本倒した
  かつ 一投目3本、二投目4本倒した
  かつ 8フレームすべてで、一投目1本、二投目1本倒した
  ならば 次のスコアになります:32
  
シナリオ: 違う初心者
  前提 新しいゲームを始める
  もし 次のピンを倒した:	2,7,3,4,1,1,5,1,1,1,1,1,1,1,1,1,1,1,5,1
  ならば 次のスコアになります:40
  
シナリオ: すべてストライク 
  前提 新しいゲームを始める
  もし すべてストライク
  ならば 次のスコアになります:300
  
シナリオ: 最初だけスペア 
   前提 新しいゲームを始める
   もし 次のピンを倒した: 2,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
   ならば 次のスコアになります:29
   
シナリオ: すべてスペア 
  前提 新しいゲームを始める
  もし 10フレームすべてで、一投目1本、二投目9本倒した
  かつ 1本倒した
  ならば 次のスコアになります:110

```

違う書き方として、次のようにも書けます。

```
機能: ボウリングスコア計算(別の方法)
  トータススコアを計算するシステムがほしい
  
シナリオ: 最初だけスペア
  前提 新しいゲームを始める
  もし 次のピンを倒した:	3,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
  ならば 次のスコアになります:29
  
シナリオ: すべてスペア
  前提 新しいゲームを始める
  もし 10フレームすべてで、一投目1本、二投目9本倒した
  かつ 1本倒した
  ならば 次のスコアになります:110

シナリオ: 別の初心者
  前提 新しいゲームを始める
  もし 次の本数を倒した
  | 本数 |
  |	2   |
  |	7	|
  |	1	|
  |	5	|
  |	1	|
  |	1	|
  |	1	|
  |	3	|
  |	1	|
  |	1	|
  |	1	|
  |	4	|
  |	1	|
  |	1	|
  |	1	|
  |	1	|
  |	8	|
  |	1	|
  |	1	|
  |	1	|
  ならば 次のスコアになります:43
  
シナリオテンプレート: スコアテーブル 
  前提 新しいゲームを始める
  もし 次のピンを倒した: <ピン>
  ならば 次のスコアになります:<スコア>

  サンプル: スコアテーブル	
  | ゲーム           | ピン                                     | スコア |
  | 初心者           | 2,7,3,4,1,1,5,1,1,1,1,1,1,1,1,1,1,1,5,1 | 40    |
  | 最初だけスペア    | 2,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 | 29    |

```

## 日本語と英語のマッピング

featureファイルの```前提```は、stepファイルの```Given```にあてはまります。

同じように日本語と英語のマッピングは、次のページのLanguageがJapaneseの部分を参照してください。
https://github.com/techtalk/SpecFlow/blob/master/Languages.xml

## 説明

簡単に説明するために以下の部分だけを見てみます。

```
シナリオ:ガーター 
  前提 新しいゲームを始める
  もし すべてガーター
  ならば 次のスコアになります:0
```

SpecFlowは、
1.featureファイルの前提＝Givenなので、Stepsファイルの```Given``` Attributeの```新しいゲームを始める```を探しにいき、そのメソッドを実行します。
2.もし=Whenなので、Stepsファイルの```When``` Attributeの```すべてガーター```を探しにいき、そのメソッドを実行します。
3.同じように、ならば=Thenなので、```Then``` Attributeの```次のスコアになります(\d+)```を探しにいき、そのメソッドを実行します。ここでは(\d+)のように正規表現で記述できます。この正規表現の値がメソッドの引数に渡されます。そして、このメソッドのアサーションの成否でテストの成否が決まります。

違うパターンとして以下の部分を見てみます。

```
シナリオテンプレート: スコアテーブル 
  前提 新しいゲームを始める
  もし 次のピンを倒した: <ピン>
  ならば 次のスコアになります:<スコア>

  サンプル: スコアテーブル   
  | ゲーム           | ピン                                     | スコア |
  | 初心者           | 2,7,3,4,1,1,5,1,1,1,1,1,1,1,1,1,1,1,5,1 | 40    |
  | 最初だけスペア    | 2,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 | 29    |
```

上記と動きは同じで、特別な部分は、
1.シナリオテンプレートが```スコアテーブル```になっているので、```サンプル```の```スコアテーブル```を探します。
2.```<>```で囲まれている文字列とテーブルのカラム名を見て、正規表現の場所に順番に渡していきます。
になります。

# 実行

Specsプロジェクトで右クリックで、Run SpecFlow Scenariosで実行できます。
featureファイルに記述している内容がstepファイルにあるのにエラーになる場合などに、Regenerate Feature Filesをやるとうまくいきます。

# Tips
* テストのあるタイミングでフックしたい場合は、以下を参照してください。   
http://www.specflow.org/documentation/Hooks/