WebdriverIOを使ってJSで同期的にE2Eテストを書く

# WebdriverIOとは

- Selenium Webdriverを利用し、ブラウザのE2E(End to End)テストをJavaScript(Node.js)で記述してブラウザ上で操作してテストするライブラリです。
- JavaScriptで普及しているテストフレームワーク([Mocha](http://mochajs.org/), [Jasmine](https://jasmine.github.io/), [Cucumber](https://cucumber.io/))で記述できます。

# [V4.0](http://webdriver.io/guide/getstarted/v4.html)から同期(synchronous)で記述可能に!

- V3.0では、[generator](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/function*)を使って`yield`で記述できました。
- V4.0では、[fiber](https://github.com/laverdet/node-fibers)を組み込み同期(synchronus)で記述できるようになりました。

## 非同期での記述方法(`sync:false`の場合)
```js
describe('asynchronus test',()=>{
  it('Promise',()=>{
    browser.url('/')
    browser.getTitle().then( title =>{
      assert( /WebdriverIO/.test(title) ) 
    })
  })
})
```

- `Promise`に対して`then`のブロックで`assert`します。

## `generator`での記述方法(V3.0で記述できた)
```js
describe('asynchronus test',()=>{
  it('Generator',function* (){
    browser.url('/')
    const title = yield browser.getTitle()
    assert( /WebdriverIO/.test(title) ) 
  })
})
```

- テストコードを`generator`で記述`function* (){}`、`yield`で戻り値を取得できます。Promiseの時よりコールバックがひとつ減ります。
- V4.0では同期で記述できるようになったため、`generator`のサポートは無くなりました。https://github.com/webdriverio/webdriverio/issues/1159

## V4.0でサポートした同期(synchronus)の記述方法

```js
describe('synchronus test',()=>{
  it('get title',()=>{
    browser.url('/')
    assert( /WebdriverIO/.test( browser.getTitle() ) )
  })
})
```
- テストコードを直感的に記述できる !!!

# 実際に試してみる

## 準備

`mocha` + `power-assert`で記述したテストコードを`PhantomJS`で実行するサンプルを用意したので、以下のように試してみましょう。
また、babelも組み込んでいるので、ES2015で記述できます。

```bash
$ git clone git@github.com:yoshi6jp/wdio4-sample.git
$ cd wdio4-sample/
$ npm install
```
## 実行結果

```bash
$ npm run test
      :
[phantomjs #0-0] Running: phantomjs
[phantomjs #0-0]
[phantomjs #0-0]   sync: test
[phantomjs #0-0]       ✓ get title
[phantomjs #0-0]
[phantomjs #0-0]
[phantomjs #0-0] 1 passing (6s)
[phantomjs #0-0]

```
- こんな感じに表示されるはずです。(spec reporterを利用しています。)

## 非同期(`sync:false`)のサンプルコードの実行結果
```bash
$ npm run async
     :
[phantomjs #0-0]
[phantomjs #0-0]   sync: false
[phantomjs #0-0]       ✓ Promise
[phantomjs #0-0]
[phantomjs #0-0]
[phantomjs #0-0] 1 passing (2s)
[phantomjs #0-0]

```
- wdio.conf.jsで`sync:false`と設定すると従来のPromiseのインタフェースが利用できます。

# 同期型のテストコードに非同期処理を組み込む場合
テストシナリオによっては、非同期処理を組み込みたい場合があります。
例えば、テストの延長で
- 表示内容をローカルファイルに書き込み
- WebAPIでたのWebサービスとRESTで連携
などです。
ファイル書き込みの場合は、[fs.writeFileSync](https://nodejs.org/api/fs.html#fs_fs_writefilesync_file_data_options)のような同期型APIを利用すれば解決できますが、非同期型APIしか提供されていないライブラリを利用するユースケースもあります。
以下のようなPromiseインタフェースを持った処理をテストシナリオに組み込む例について解説します。

```js
/**
 * @returns {Promise}
 */
const asyncFn = ()=> new Promise((resolve, reject)=>{
  setTimeout(()=>{
    console.log("--- 1 ---")
    resolve()
  },1000)
})
```

## Promiseで実装
単純に`Promise`の`then`のブロックに後の処理を記述します。

```js
describe('asynchronus',()=>{
  it('Promise', ()=>{
    return asyncFn().then( () => {
      console.log("--- 2 ---")
      browser.url('/')
      assert( /WebdriverIO/.test( browser.getTitle() ) )
      console.log("--- 3 ---")
    })
  })
})
```

### 実行結果

```bash
$ npm run sync
     :
--- 1 ---
--- 2 ---
     :
[phantomjs #0-0] Running: phantomjs
[phantomjs #0-0]
[phantomjs #0-0]   sync: true
[phantomjs #0-0]
[phantomjs #0-0]     asynchronus
[phantomjs #0-0]         1) Promise
[phantomjs #0-0]
[phantomjs #0-0]
[phantomjs #0-0] 1 failing (3s)
[phantomjs #0-0]
[phantomjs #0-0] 1) asynchronus Promise:
  assert(/WebdriverIO/.test(browser.getTitle()))
                       |    |       |           
                       |    |       Object{defer:#defer#,promise
                                 :
                       false   
```
- `then`のブロックの内部だと`browser.getTitle()`がPromiseを返すようになり、エラーになってしまいます。

## Fiberで実装
`it`のブロックの内部は、Fiberの空間になっているので、Fiberのインタフェースで記述します。

```js
import Fiber from 'fibers'

            :
describe('asynchronus',()=>{
  it('Fiber',()=>{
    const fiber = Fiber.current
    asyncFn().then(()=>
      fiber.run()
    })
    Fiber.yield()
    console.log("--- 2 ---")
    browser.url('/')
    assert( /WebdriverIO/.test( browser.getTitle() ) )
    console.log("--- 3 ---")
  })
})
```
- 非同期処理を開始した後、一旦`Fiber.yield()`で処理を中断し、非同期処理が完了した後に`fiber.run()`で処理を再開しています。

### 実行結果
```bash
$ npm run sync
    :
--- 1 ---
--- 2 ---
--- 3 ---
    :

[phantomjs #0-0] Running: phantomjs
[phantomjs #0-0]
[phantomjs #0-0]   sync: true
[phantomjs #0-0]
[phantomjs #0-0]     asynchronus
[phantomjs #0-0]         ✓ Fiber
[phantomjs #0-0]
[phantomjs #0-0]
[phantomjs #0-0] 1 passing (7s)
[phantomjs #0-0]

```
- 実行結果も期待通りの動作になりました。

## [browser.call](http://webdriver.io/api/utility/call.html)で実装
WebdriverIOが提供しているAPIの[browser.call](http://webdriver.io/api/utility/call.html)を利用することで、もっとシンプルに実装できます。

```js
describe('asynchronus',()=>{
  it('browser.call',()=>{
    browser.call(()=> asyncFn())
    console.log("--- 2 ---")
    browser.url('/')
    assert( /WebdriverIO/.test( browser.getTitle() ) )
    console.log("--- 3 ---")
  })
})
```
- `browser.call`は、引数として`Promise`を戻り値とする関数を渡す必要があります。

### 実行結果
```bash
$ npm run sync
    :
--- 1 ---
--- 2 ---
--- 3 ---
    :
[phantomjs #0-0] Running: phantomjs
[phantomjs #0-0]
[phantomjs #0-0]   sync: true
[phantomjs #0-0]
[phantomjs #0-0]     asynchronus
[phantomjs #0-0]         ✓ browser.call
[phantomjs #0-0]
[phantomjs #0-0]
[phantomjs #0-0] 1 passing (8s)

```

# その他の便利な機能

## [$](http://webdriver.io/api/utility/$.html), [$$](http://webdriver.io/api/utility/$$.html)

V4.4から追加されたAPIです。それぞれ`browser.element` `browser.elements`のショートカットとしてグローバルスコープに定義されています。

```js
  // ボタンをクリック
  $("button.btn").click()
```

このように`$`(document.querySelector), `$$`(document.querySelectorAll)と同じように、テストコードを記述することができます。


## [Repl](http://webdriver.io/guide/usage/repl.html)

V4.5からReplが追加されました。Replから直接ブラウザを操作できるので、APIやセレクターの動作を確認しながら、テストコードが記述できます。

### 準備

ヘッドレスブラウザ(PhantomJS)だと動作が分かりにくいので、`selenium-standalone`を使ってインストールします。

```bash
$ npm install selenium-standalone
$ ./node_modules/.bin/selenium-standalone install
----------
selenium-standalone installation starting
               :
selenium-standalone installation finished
-----
```
インストールが完了したら`selenium`を起動します。

```bash
$ ./node_modules/.bin/selenium-standalone start
```

別のターミナルを開いてReplを起動します。同時にブラウザ(Chrome)も起動します。

```bash
$ ./node_modules/.bin/wdio repl chrome
[21:09:49]  DEBUG	Queue has stopped!
[21:09:49]  DEBUG	You can now go into the browser or use the command line as REPL
[21:09:49]  DEBUG	(To exit, press ^C again or type .exit)
```

```bash
> browser.url("http://webdriver.io")
> $$("nav a")[2].click()
```
後は、ReplでAPIを試すことができます。実際にブラウザで動作することが確認できると思います。

# まとめ

- `Webdriver.IO`を使うとJavaScriptで同期的にテストを記述できます。
- `mocha` `power-assert` `$` `$$`など、ユニットテストを書くのと同じような感覚でE2Eテストを記述できます。