Gauge + PuppeteerでATDD(受け入れテスト駆動開発)をはじめよう

# ATDDとは

受け入れテスト駆動開発。Acceptance Test Driven Developmentの略です。

[実践テスト駆動開発](https://www.amazon.co.jp/実践テスト駆動開発-Object-Oriented-SELECTION-Freeman/dp/4798124583)
で推奨されているATDDは、受け入れテスト(UAT)のTDDループとユニットテストのTDDループを2重でぐるぐる回すというものになっています。

## 外側のループ: 受け入れテスト

* 失敗する受け入れテスト(UAT)を書く
* ユニットテスト＆実装ループ
* 受け入れテストがパスする

## 内側のループ: ユニットテスト

* 失敗するユニットテストを書く
* ユニットテストを最短でパスさせる
* リファクタリング

ということでまずは失敗する受け入れテストを書くことから開発が始まるわけです。

# Gaugeとは

Gaugeはユーザ受け入れテスト(UAT)をMarkdown形式で自然言語で書くためのテストフレームワークです。
Markdownで書くと言ってもすべてをMarkdownで書くわけではなく、テスト仕様の表面的な部分のみMarkdownで定義し、具体的なコンポーネントへのアクセスや通信処理等は下の技術レイヤーで行います。
技術レイヤーとはGaugeプラグインにより橋渡ししており、特定の言語やテストフレームワークへの依存がないようになっています。

# Gaugeを使って日本語でテストを書く

今回のサンプルアプリケーションは、ローカルのファイルをWebにアップロードする機能と、アップロードされたファイルの一覧が表示できる機能があるものとします。

ファイルアップロード機能のテストシナリオを書いてみます。

```
# ファイルアップロード機能

## テキストファイルをアップロードできること

* ファイル名が"test2.txt"、内容が"brabrabra"のテキストファイルを作成する
* ファイル一覧の件数が"0"であること
* ファイル"test2.txt"をアップロードする
* ファイル一覧の件数が"1"であること
```

このようにMarkdown形式で、日本語で書けるので非常に可読性が高いテストになります。
顧客やプロダクトオーナーによるテスト定義やレビュー関与も現実的なのではないかと思います。

# Gauge環境設定

作成したテストシナリオを実行するためにGaugeの環境をセットアップします。

* まずはGaugeをインストール (Macの場合の例)

```
brew install gauge
```

参考: https://github.com/getgauge/gauge 

* 適当なディレクトリに移動し、Gaugeプロジェクトとして初期化します。プラグインはJavaScript/Puppeteerを利用することにします。

```
mkdir sample-app
cd sample-app

gauge init js_puppeteer
```

参考: https://github.com/getgauge/gauge-js

* プラグインをインストールすると自動で入るサンプルコードは不要なので削除しておきます。

```
rm specs/example.spec
rm tests/step_implementation.js
```

* テストシナリオをGaugeプロジェクト配下に移動します。

```
mv file-upload.spec specs
```

* テストを実行します。

```
gauge run specs
```

この段階では具体的にどうテストするのかを定義していないので、テストの実行は以下のようにスキップされます。

```
[ValidationError] /Users/ikeda/repo/box/qiita/gauge-puppeteer/specs/file-upload.spec:5 Step implementation not found => 'ファイル名が"test2.txt"、内容が"brabrabra"のテキストファイルを作成する'
[ValidationError] /Users/ikeda/repo/box/qiita/gauge-puppeteer/specs/file-upload.spec:6 Step implementation not found => 'ファイル一覧の件数が"0"であること'
[ValidationError] /Users/ikeda/repo/box/qiita/gauge-puppeteer/specs/file-upload.spec:7 Step implementation not found => 'ファイル"test2.txt"をアップロードする'
[ValidationError] /Users/ikeda/repo/box/qiita/gauge-puppeteer/specs/file-upload.spec:8 Step implementation not found => 'ファイル一覧の件数が"1"であること'
Add the following missing implementations to fix `Step implementation not found` errors.

step("ファイル名が<arg0>、内容が<arg1>のテキストファイルを作成する", async function(arg0, arg1) {
	throw 'Unimplemented Step';
});
step("ファイル一覧の件数が<arg0>であること", async function(arg0) {
	throw 'Unimplemented Step';
});
step("ファイル<arg0>をアップロードする", async function(arg0) {
	throw 'Unimplemented Step';
});
Successfully generated html-report to => /Users/ikeda/repo/box/qiita/gauge-puppeteer/reports/html-report/index.html
Specifications:	0 executed	0 passed	0 failed	1 skipped
Scenarios:	0 executed	0 passed	0 failed	1 skipped
```

# Puppeteerとは

ヘッドレスChromeを操作するライブラリです。
ブラウザはChrome限定ですが、Seleniumに比べると技術スタックも使い方もシンプルなので取っ掛かりやすいです。

参考: https://github.com/GoogleChrome/puppeteer

# Puppeteerでテストを書く

Markdownのシナリオファイルで書いたテストの各ステップをJavaScript/Puppeteerで定義します。

`tests/step_implementation.js`を以下の内容で作成します。

```
const puppeteer = require('puppeteer');
const assert = require('assert');
const fs = require('fs');

let browser;
let page;

beforeScenario(async function () {
  browser = await puppeteer.launch({
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox'
    ]
  });
  page = await browser.newPage();
});

afterScenario(async function () {
  browser.close();
});

step("ファイル名が<filename>、内容が<content>のテキストファイルを作成する", function(filename, content) {
  fs.writeFileSync(filename, content);
});

step("ファイル一覧の件数が<count>であること", async function(count) {
  const url = 'http://localhost:5000/';
  await page.goto(url);
  
  const items = await page.$$('a[href*=documentView]');
  assert.equal(items.length, count);
});

step("ファイル<filename>をアップロードする", async function(filename) {
  const url = 'http://localhost:5000/documentAdd';
  await page.goto(url);

  let input = await page.$("input[type=file]");
  await input.uploadFile(filename);

  let uploadButton = await page.$('button');
  await uploadButton.click();
});
```

Markdownファイルの箇条書きした1ステップを、stepに渡した関数として具体的テスト手順を定義しています。
before/afterとstepのみGaugeの言葉で、それ以外はGauge非依存で自由にテストを定義することができます。
今回はpuppeteerを使いましたが、Selenium等に置き換えることも可能です。

テストを再度実行してみます。

```
$ gauge run specs

# ファイルアップロード機能
  ## テキストファイルをアップロードできること	 ✔ ✘
        Failed Step: ファイル一覧の件数が"0"であること
        Specification: specs/file-upload.spec:6
        Error Message: Error: Failed to navigate: http://localhost:5000/
        Stacktrace:
        Error: Failed to navigate: http://localhost:5000/
            at Page.goto (node_modules/puppeteer/lib/Page.js:381:13)
            at <anonymous>

Successfully generated html-report to => /Users/ikeda/repo/box/qiita/gauge-puppeteer/reports/html-report/index.html
Specifications:	1 executed	0 passed	1 failed	0 skipped
Scenarios:	1 executed	0 passed	1 failed	0 skipped
```

結果がSkippedからFailedに変わりました。
`localhost:5000`にアクセスできないというエラーです。
Webサーバを起動もしていないし、実装も全くしていないので当然ですね。

この次は失敗するユニットテストを書くフェーズに移ります。