Karateを利用したUIテスト（もしくはE2Eテスト）

# はじめに

## UIテストにおける潮流
UIテストの自動化の代表格といえば、言わずとしれた [Selenium](https://www.seleniumhq.org/) でしょう。
最近では、Selenium をラップして、より使いやすくした [Selenide](https://selenide.org/) も利用されているケースが増えているように思います。

自分自身も、Selenium のHTMLでテストシナリオを作成できる点や、[Selenium IDE](https://chrome.google.com/webstore/detail/selenium-ide/mooikfkahbdckldjjndioackbalphokd?hl=ja) との組み合わせの良さに感激し、以前は積極的に利用していたものです。

ただ、上記の **ノンプログラミングモデル** だと、自由度が低く、ある程度の制限も出てくることから、最近では、WebDriverを利用した **プログライミングモデル** が主流になっているかと思います。
このモデルは、様々な言語に対応しており、多くの開発プロジェクトで利用できることは良いことなのですが、 **プログラミングモデル** の場合、個人的には、対応できるテストエンジニアに限りがあったり、メンテナンスコストの高さが課題となってくると感じています。


## Karateの登場
そこで **[Karate](https://intuit.github.io/karate/)** の登場です。
Karateが何か、というのは、こちらを。

> Karateは、CucumberというBDD（ビヘイビア駆動開発）を行うためのフレームワークをJavaVM上に移植したもので、Gherkinの文法を自然言語に近い形で記述できるユニットテストツールです。
> 
> [Karateとは？](https://qiita.com/takanorig/items/11fd65a4359280cb82e5)

Karate は、WebアプリにおけるAPIベースのテストを自動化するツールですが、現在は、WebDriverを使ったUIテストも可能になっています。
私自身、APIテストだけでなく、UIテストにも、Karateを利用し出しています。

Gherkin 形式の DSL（Domain Specific Language） でテストシナリオを直感的に作成できる一方で、そのシナリオの中で、JavaScriptの関数を定義して実行したり([JS Functions](https://intuit.github.io/karate/#javascript-functions))、Javaのクラスを呼び出したり（[Java Interop](https://intuit.github.io/karate/#calling-java)）することも可能なので、例えば、以下のことをひとつのツールで実現することができます。

* テストの事前処理として、DBを初期化する（サーバー）。
* ブラウザから項目入力をして、データを登録する（画面）。
* 登録された結果を、画面で確認する（画面）。
* 画面には表示されないが、DBに必要な項目が登録されているかを確認する（サーバー)。

Selenium のノンプログラミングモデルの場合、画面側の確認はできても、サーバー側の処理や確認はできません。そのため、以前は、初期化やデータ確認用のツールやAPIを別途開発して、試験を実施したりしていました。
このようなところが、UIテストやE2Eテストを、効率よく作成／実行する上では、大きな差になってきます。

Karateでは、DSLによる直感的なテストシナリオの作成と、複雑な処理はJavaScriptやJavaのコード実行に委譲できる、ハイブリッド構成が取れる点が、利便性の高さだと感じています。


# Karate Driver
Karate におけるUIテストは、 「Karate Driver」というWebDriverベースの機能を利用します。

* https://intuit.github.io/karate/karate-core/
* https://github.com/intuit/karate/tree/master/karate-demo/src/test/java/driver/demo

ということで、まずは KarateDriver でのテスト内容について、実際のシナリオを以下に示します。

## ブラウザ操作のシナリオ
サンプルとして、GitHubにおけるリポジトリ検索の操作を自動化してみました。

```cucumber
Feature: GitHubに対するUI操作

Background:
  * configure driver = { type: 'chrome' }

# -----------------------------------------------
# GitHubでのリポジトリ検索
# -----------------------------------------------
Scenario: Search intuit/karate in GitHub

    * def keyword = 'karate'

    Given driver 'https://github.com/search'
        And eval driver.waitUntil(driver.title == 'Code Search · GitHub')
        And driver.input('input[name=q]', keyword)
    When driver.submit('#search_form button.btn')
    Then eval driver.waitUntil(driver.location == 'https://github.com/search?utf8=%E2%9C%93&q=' + keyword + '&ref=simplesearch')
        And eval driver.title == 'Search · karate · GitHub'

    * print driver.text('li.repo-list-item h3:first-child a')
    * match driver.text('li.repo-list-item h3:first-child a') == 'intuit/karate'

    * def bytes = driver.screenshot()
    * eval karate.embed(bytes, 'image/png')
```

以下、ポイントとなる内容を見ていきます。

```cucumber
Background:
  * configure driver = { type: 'chrome' }
```

利用するブラウザ（WebDriver）の指定です。

| type | 説明 |
| ---- | ---- |
| chrome | “native” Chrome automation via the DevTools protocol |
| chromedriver | W3C Chrome Driver |
| geckodriver  | W3C Gecko Driver (Firefox) |
| safaridriver | W3C Safari Driver |
| mswebdriver | Microsoft WebDriver |
| msedge | Microsoft Edge |


```cucumber
    Given driver 'https://github.com/search'
        And eval driver.waitUntil(driver.title == 'Code Search · GitHub')
        And driver.input('input[name=q]', keyword)
```
```Given``` で、最初に遷移する画面と、その画面での入力を行っています。
```driver.waitUntil()``` を利用して、画面が表示されてから、入力を行うようにしています。

```driver.input（）``` では、CSSセレクタを利用して、要素を特定することができます。
これは、他のコマンドでも同様で、CSSセレクタで要素を特定することができますし、他にも、XPathなども指定することができます。


```cucumber
    When driver.submit('#search_form button.btn')
```
formのsubmit処理です。
上記は、検索ボタンを押下する内容になります。


```cucumber
    Then eval driver.waitUntil(driver.location == 'https://github.com/search?utf8=%E2%9C%93&q=' + keyword + '&ref=simplesearch')
        And eval driver.title == 'Search · karate · GitHub'
```
検索後の内容の確認です。
```driver.waitUntil()``` で、検索後の画面の表示を待ってから、処理をしています。


```cucumber
    * match driver.text('li.repo-list-item h3:first-child a') == 'intuit/karate'
```
ここは、表示されている要素の確認です。
```driver.text()``` で、表示されている内容を確認しています。


```cucumber
    * def bytes = driver.screenshot()
    * eval karate.embed(bytes, 'image/png')
```
さらに、上記のようにすると、画面キャプチャを取得し、Karateのレポートに挿入してくれます。

![karate-driver-report.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22893/db9ee707-a61b-8b36-2468-95c5f041d52a.jpeg)

<font color="#ff0000" size="5">
**これは、 シナリオの内容と、キャプチャが一体化してくれて、便利！！**
</font>
さらに、printコマンドを使って、コメントなどを適宜出力しておけば、どのような試験で、どのような状態になったか、一目瞭然ですね。

## JavaScriptによる制御を行うシナリオ
上記では、ボタンを押下する処理を

```cucumber
    When driver.submit('#search_form button.btn')
```

として行っていましたが、この部分を、JavaScriptによる制御に置き換えてみます。

```cucumber
# -----------------------------------------------
# GitHubでのリポジトリ検索
#   ・ブラウザ内でのJavaScript実行あり
# -----------------------------------------------
Scenario: Search2 intuit/karate in GitHub

    * def keyword = 'karate'

    # ブラウザで動作させるJSは、Karate自体の JS Function とは定義方法が異なる。
    # 関数自体は、文字列として定義し、それを driver.eval で実行させる。
    # 以下は、JS Function を文字列として定義し、変数部分は replace を使って置換している。
    * text formSubmitWebFn =
        """
        var formElem = document.querySelector('${selector}');
        formElem.submit();
        """
    * replace formSubmitWebFn.${selector} = '#search_form'

    Given driver 'https://github.com/search'
        And eval driver.waitUntil(driver.title == 'Code Search · GitHub')
        And driver.input('input[name=q]', keyword)
    When eval driver.eval(formSubmitWebFn)
    Then eval driver.waitUntil(driver.location == 'https://github.com/search?utf8=%E2%9C%93&q=' + keyword + '&ref=simplesearch')
        And match driver.title == 'Search · karate · GitHub'

    * print driver.text('li.repo-list-item h3:first-child a')
    * match driver.text('li.repo-list-item h3:first-child a') == 'intuit/karate'

    * def bytes = driver.screenshot()
    * eval karate.embed(bytes, 'image/png')
```
これも、最初のシナリオと同様の動作をします。

```cucumber
    * text formSubmitWebFn =
        """
        var formElem = document.querySelector('${selector}');
        formElem.submit();
        """
    * replace formSubmitWebFn.${selector} = '#search_form'
```

ここでは、上記のように、「text」を使って、ブロックテキストとしてJavaScriptを定義した後、「replace」で変数部分を置換しています。

```cucumber
    * eval driver.eval(formSubmitWebFn)
```
定義したJavaScriptの関数は、上記のようにすることで、ブラウザ上で実行することができます。
このようにして、標準のコマンドだけではできない内容も、JavaScriptの関数を組み合わせることで、複雑な操作も実現可能になります。

## サンプルコード
今回作成したサンプルの内容は、以下に登録しています。

* https://github.com/takanorig/example-karate/tree/master/src/test/java/examples/uiautomation


また、本記事では、Javaコードによる処理は行っていませんが、シナリオ中にJavaコードを呼び出しての処理をすることも可能です。
これについては、以下を参照してください。

* https://intuit.github.io/karate/#calling-java
* https://qiita.com/takanorig/items/70f14fa6ef024850ba25#%E6%8B%A1%E5%BC%B5

## 注意点

#### Given-When-Then の部分利用
今回は、Given-When-Thenの内容をセットで利用していますが、実は、部分的に実行することも可能です。

```cucumber
# 画面遷移を確認
Given driver baseUrl + '/path/to'
    And driver.waitUntil(driver.title == '画面 | アプリケーション')
Then match driver.text('#title') == '画面タイトル'
```

```cucumber
# ダイアログ表示を確認
When driver.click('#dialog-open')
Then eval driver.text('div.modal-mask div.header') == 'ダイアログ'
```

UIの操作では、必ずしもフォーム入力をしてsubmitを行うような内容だけではないので、上記のように、部分的な利用もできることが分かっていると、シナリオ作成の幅が広がります。


#### driver.waitUntil() の利用方法
今回、以下のように、```waitUntil``` の中で、KarateDriverの関数である ```driver.title``` を利用していますが、ここは、任意のコマンドを実行できるわけではありません。

```
driver.waitUntil(driver.title == 'Code Search · GitHub')
```

Karateのドキュメントを見ると、本来は、以下のように、JavaScriptの式を指定する内容になっています。
タグの内容やCSSをの内容を見て、状態が更新されるのを待つ、など、そのような使い方をします。

```
driver.waitUntil("document.readyState == 'complete'")
```

そこで、なぜ ```driver.title``` を使えるか、というと、今回のサンプルの内容は、実は以下のようにKarate内部で変換されて実行されています（```driver``` = ```document``` ということではありません）。

```
driver.waitUntil(driver.title == 'Code Search · GitHub')
 ↓
driver.waitUntil(document.title == 'Code Search · GitHub')
```


# 補足
今回示した内容以外に、Karate Driver では、以下のようなことも可能です。

* Windowsアプリの操作（[Example](https://intuit.github.io/karate/karate-demo/src/test/java/driver/windows/calc.feature)）
* Cookie に対する操作
* ブラウザダイアログに対する処理

その他、本記事で利用した以外のコマンドも、多く用意されており、その辺りは、以下のあたりの内容を参照してください。

* https://intuit.github.io/karate/karate-core/#js-api

よくUIテストで課題になる、ファイルアップロード／ダウンロードなどの処理は、現状では、工夫しないと実現できないのですが、その辺りは、今後の拡張に期待です（画面操作ではなく、APIに対する処理であれば可能です）。


# まとめ
今回は、Karate Driver を利用した、UIテスト（E2Eテスト）の内容を紹介しました。
まだまだ発展途上の機能ですが、簡単にシナリオを作成して、UI操作を自動化できるのは、テストだけではなく、通常の業務自動化にも利用できるのではないか、と別の期待も持てる内容でした。

特に、シナリオの実行内容のレポートに、自動で画面キャプチャを埋め込めるのは、私もお気に入りの機能です。
**これで、スクリーンショットを撮ってExcelに貼り付ける、なんてことをやって人生の時間を浪費している人を、救うこともできそうです（笑）。**