Drupal 向け Behat の拡張その2 カスタムステップの作成（後編）

前回、[Drupal 向け Behat の拡張その1 複数ステップの実行とカスタムステップの作成（前編）](http://qiita.com/tom_konda/items/01ec5040d9cc3c04c5a6)という記事を書きました。その記事では、シナリオのステップを複数実行とパラメータを持たないカスタムステップの作成をしました。今回は、ステップにパラメータがあるカスタムステップの作成を行います。

## パラメータがあるカスタムステップの作成

### 正規表現を使用しないステップ

作成するテストシナリオは下記になります。

<dt>Feature 名</dt>
<dd>テストフィーチャー3</dd>
<dt>シナリオ名</dt>
<dd>カスタムステップの作成2</dd>
<dt>前提条件</dt>
<dd>ユーザはログインしていない</dd>
<dt>行う動作</dt>
<dd>指定文字列（例：hogehoge）で始まるランダムなアルファベット6〜10文字の URL にアクセス</dd>
<dt>期待する結果</dt>
<dd>ステップ実行時の URL を表示する</dd>

Feature 名はパラメータがないカスタムステップと同じですが、シナリオ名は異なっています。Behat では 1つの Feature に複数のシナリオを含むことが出来るので、前回作成したシナリオの下に続けてシナリオを書くことが出来ます。上記のテストシナリオは下記のようになります。

```gherkin:test3.feature
  Scenario: カスタムステップの作成2
    # シナリオの中身
    Given I am not logged in
    When I go to random URL start with "hogehoge"
    Then print current URL
```

未定義ステップのエラーメッセージに作成する関数のヒントは下記のようになります。

```bash
--- FeatureContext has missing steps. Define them with these snippets:

    /**
     * @When I go to random URL start with :arg1
     */
  public function printCurrentDate() :void
  {
    echo (new DateTime())->format('Y/m/d H:i:s');
  }
```

`:arg1` という部分がステップで指定するパラメータとなります。`:arg1` は定義済みのステップを表示する時にも出力されるので、後で分かりやすい名前に変更しておきます。
ページ遷移の実装は、パラメータがない時のカスタムステップで確認したファイルにある、`When /^(?:|I )go to "(?P<page>[^"]+)"$/` のステップの[ソースコード](https://github.com/Behat/MinkExtension/blob/160d4b77095dee2b4379e9a312977e8dcdf01b00/src/Behat/MinkExtension/Context/MinkContext.php#L38)を参考にします。

```php:MinkContext.php
    /**
     * Opens specified page
     * Example: Given I am on "http://batman.com"
     * Example: And I am on "/articles/isBatmanBruceWayne"
     * Example: When I go to "/articles/isBatmanBruceWayne"
     *
     * @Given /^(?:|I )am on "(?P<page>[^"]+)"$/
     * @When /^(?:|I )go to "(?P<page>[^"]+)"$/
     */
    public function visit($page)
    {
        $this->visitPath($page);
    }
```

`$this->visitPath($page);` で遷移できることが分かります。最終的にソースコードは以下のようになります。

```php:FeatureContext.php
  /**
   * @When I go to random URL start with :start
   */
  public function iGoToRandomUrlStartWith(string $start) :void
  {
    $next_url = strlen($start) ? "{$start}/" : "";
    $url_length = random_int(6, 10);
    
    $create_random_url = function (int $length = 8) :string {
      $random_url = "";
      for($i = $length; $i; $i--) {
        $seed = random_int(65, 90);
        $is_capital = random_int(0, 1);
        $random_url .= $is_capital ? chr($seed) : chr($seed + 32);
      }
      return $random_url;
    };
    
    $next_url .= $create_random_url($url_length);
    $this->visitPath($next_url);
  }
```

余談ですが、`When /^(?:|I )go to "(?P<page>[^"]+)"$/` のステップの実装は、先述のソースコードを見ると分かるとおり `Given`、`When` の両方で使えるようになっています。このように、コメントのところで `Given`、`When`、`Then` での記述の仕方をそれぞれ設定しておけば、それぞれのところでステップが使用可能になります。

### 正規表現を使用するステップ

先ほど作成した正規表現を使わないカスタムステップでは、必ず `When I go to random URL start with :start` という形であるため、毎回主語の "I" を書かなければなりません。前回説明した `When /^(?:|I )go to "(?P<page>[^"]+)"$/` のように主語の "I" がなくても動作するように正規表現を用いて柔軟なカスタムステップを作成します。

作成するテストシナリオは下記になります。

<dt>Feature 名</dt>
<dd>テストフィーチャー3</dd>
<dt>シナリオ名</dt>
<dd>カスタムステップの作成3</dd>
<dt>前提条件</dt>
<dd>ユーザはログインしていない</dd>
<dt>行う動作</dt>
<dd>指定文字列（例：foobar）で始まるランダムなアルファベット指定文字数（例：8）の URL にアクセス</dd>
<dt>期待する結果</dt>
<dd>ステップ実行時の URL を表示する</dd>

上記のテストシナリオは下記のようになります。

```gherkin:test3.feature
  Scenario: カスタムステップの作成3
    # シナリオの中身
    Given I am not logged in
    When I go to random 8 chars URL start with "foobar"
    Then print current URL
```

Behat を実行すると、未定義のステップのヒントは下記のようになります。

```bash
--- FeatureContext has missing steps. Define them with these snippets:

    /**
     * @When I go to random :arg2 chars URL start with :arg1
     */
    public function iGoToRandomCharsUrlStartWith($arg1, $arg2)
    {
        throw new PendingException();
    }
```

このソースを `/path/to/behat/behat-tests/features/bootstrap/FeatureContext.php` に追記しますが、このままだと正規表現を使用しないパターンと変わらないのでこれを変更していきます。
正規表現を使用するとなると下記の箇所が対象になるかと思います。

- 主語の "I"
- URL の長さを決める指定文字数の数字
- 指定文字数が 1 だった時の "chars" の単数形対応
- URL を始めるときに指定する文字列

これらに正規表現を当てはめると、
`@When I go to random :arg2 chars URL start with :arg1` → 
`@When /^(|I )go to random \d+ chars? URL start with ".*"$/`
と置き換えることが出来ます。しかし、この状態では Behat を実行した時に何も PHP の関数には値を渡されません。
Behat ではパラメータとして PHP の関数に渡したい場合、PHP の正規表現のサブパターンにある `(?P<subpattern_name>regexp)` の形（名前付きサブパターン）にする必要があります。今回の場合、パラメータとして渡したいのは、URL の長さの文字数と URL の開始を指定する文字列であるため、これらを名前付きサブパターンに変更すると、
`@When /^(|I )go to random \d+ chars? URL start with ".*"$/` → 
`@When /^(|I )go to random (?P<length>\d+) chars? URL start with "(?P<start>.*)"$`
という形になります。最終的にソースコードは以下のようになります。

```php:FeatureContext.php
  /**
   * @When /^(?:|I )go to random (?P<length>\d+) chars? URL start with "(?P<start>.+)"$/
   */
  public function iGoToRandomCharsUrlStartWith(string $start, int $length):void
  {
  	$next_url = strlen($start) ? "{$start}/" : "";
  
  	$create_random_url = function (int $length = 8) :string {
  		$random_url = "";
  		for($i = $length; $i; $i--) {
  			$seed = random_int(65, 90);
  			$is_capital = random_int(0, 1);
  			$random_url .= $is_capital ? chr($seed) : chr($seed + 32);
  		}
  		return $random_url;
  	};
  
  	$next_url .= $create_random_url($length);
  	$this->visitPath($next_url);
  }
```

この実装ならば、下記のようにステップを記述しても問題なく動作するはずです。


```gherkin:test3.feature
  Scenario: カスタムステップの作成3-1
    # シナリオの中身
    Given I am not logged in
    When I go to random 1 char URL start with "foobar"
    Then print current URL

  Scenario: カスタムステップの作成3-2
    # シナリオの中身
    Given I am not logged in
    When go to random 10 chars URL start with ""
    Then print current URL
```

最後に、パラメータを取るカスタムステップを作成する場合、 PHP の実装では注意点があります。Behat では正規表現の有無にかかわらず、PHP の関数では引数の数をチェックしています。そのため、PHP の関数で可変長引数を使って実装しても正しく動作しません。

## まとめ

- パラメータを受け取るカスタムステップを作成しました
  - PHP 側の関数では引数となるパラメータの数がステップのパラメータの数と同じにする必要があります
- 正規表現を使ったカスタムステップの場合、ユーザのステップの記述が柔軟になる
  - 主語の有無や単語の単数形・複数形に対応しやすくなる
  - ただし、正規表現を使う場合はパラメータの箇所は名前付きサブパターンにする必要があります

---
### Behat on Drupal 8 の記事一覧

| 回 | タイトル |
|:-:|:-:|
|第1回|  [Drupal 8 に Drupal 向け Behat のスタンドアロンでの導入とシンプルなテストシナリオ](http://qiita.com/tom_konda/items/f9a22f55f458bb954b1f) |
|第2回|  [Drupal 向け Behat の拡張その1 複数ステップの実行とカスタムステップの作成（前編）](http://qiita.com/tom_konda/items/01ec5040d9cc3c04c5a6) |
|第3回|**Drupal 向け Behat の拡張その2 カスタムステップの作成（後編）**|