Selenide入門

[Selenium/Appium Advent Calendar 2016](http://qiita.com/advent-calendar/2016/selenium_and_appium)の15日目です。
今年から業務で[Selenide](http://selenide.org/)を使い始めました。と言っても現在のところ先行して一人で使っているだけなので、チームメンバーへの共有と自分用のリファレンス目的でまとめてみました。
※この記事は当初v4.1をベースに書きましたが、その後v4.8の変更を追記しています。

# セットアップ
## ライブラリの設定
例えばGradleの場合はbuild.gradleに追加するだけです。

```groovy
dependencies {
  testCompile 'com.codeborne:selenide:4.8'
}
```

## WebDriverの指定
Chromeを例に上げます。実際に使う時はシステムプロパティ経由で指定することが多いのかなと思いますが、お試しでJUnitで動かす場合は`@BeforeClass`にでもブラウザの指定を書いておけば良いかと思います。
~~chromedriverは[ここ](https://sites.google.com/a/chromium.org/chromedriver/downloads)からをダウンロードしておきましょう。~~
上記のように自分でダウンロードしても良いですが、Selenide4.7からは`-Dwebdriver.chrome.driver`を明示的に指定しなければ自動的に最新のChromedriverをダウンロードしてくれるようになりました。
内部的には[webdrivermanager](https://github.com/bonigarcia/webdrivermanager)を使用しているようです。

```java
Configuration.browser = WebDriverRunner.CHROME;
// (v4.7から)↓ダウンロードをSelenideに任せる場合は指定しない
System.setProperty("webdriver.chrome.driver", "driver/chromedriver");
```
Webdriverについては@hiroshitodaさんの6日目の記事が詳しいかと思います。
[Seleniumでブラウザーと一緒に用意しなきゃいけないアレの更新がめんどいのをどうにかしよう](http://qiita.com/hiroshitoda/items/8bb1c6a8947c80e275cc)

# 基本的な使い方
最新版のJavaDocは[こちら](http://selenide.org/javadoc/current/)

## サンプルコード
Google検索するサンプルです。
staticインポートを使わなかったり、無駄にローカル変数で受けたりしてますが説明のためです。

```java
@Test
public void googleSearch() {
    // ブラウザでGoogleを開く
    Selenide.open("http://www.google.com/");

    // Google検索
    SelenideElement element = Selenide.$(By.name("q"));
    element.val("selenide").pressEnter();

    // 検索結果を確認
    ElementsCollection results = Selenide.$$(By.cssSelector("#ires .g"));
    results.shouldHaveSize(10);
    results.get(0).shouldHave(Condition.text("Selenide: concise UI tests in Java"));
}
```

## 単項目の取得
JQueryライクに `$()` で要素を取得できます。

```java
// CSSセレクタで検索する書き方
SelenideElement elm1 = $("#btn");
// SeleniumのByクラスを使用する書き方
SelenideElement elm2 = $(org.openqa.selenium.By.cssSelector("#btn"));
```
- `SelenideElement`はSeleiniumの`WebElement`を継承して便利な機能が追加されています。
- 複数要素が取得されるようなセレクタを指定した場合は先頭の要素が返されます。
- 先頭以外の要素を取得したい場合は第2引数にインデックスを指定して取得することが可能です。

## 複数項目の取得
`$$()` で複数要素を配列で取得できます。

```java
ElementsCollection elms1 = $$(".btn");
ElementsCollection elms2 = $$(org.openqa.selenium.By.cssSelector("#btn"));
```

- ElementsCollectionは`java.util.AbstractList`を継承したクラスで、SelenideElementの集合を扱うクラスです。  
- Listインタフェースのメソッドに加え、 `first`、 `last`、`filter` など便利なメソッドが追加されています。

## Byの生成
Selectorsクラスに`org.openqa.selenium.By`を生成するファクトリーメソッドが用意されています。
CSSをセレクタを書くのが面倒なものはこちらを使えば良いでしょう。

|メソッド|説明|
|-------|------------|
|public static By withText(String elementText)|指定した文字を **含む** textを検索する|
|public static By byText(String elementText)|指定した文字に **一致する** textを検索する|
|public static By byAttribute(String attributeName, String attributeValue)|指定した要素名、値を検索する。(byも等価)|
|By byTitle(String title)|titleで検索する|
|By byValue(String value)|valueで検索する|


org.openqa.selenium.Byのメソッドをラップしてるだけのものも結構ありますがお好みで。（例えばbyNameはBy.nameをラップしてるだけ）

- public static By byName(String name)
- public static By byXpath(String xpath)
- public static By byLinkText(String linkText)
- public static By byPartialLinkText(String partialLinkText)
- public static By byId(String id)
- public static By byCssSelector(String css)
- public static By byClassName(String className)

## 親要素/子要素の検索

取得済みの要素を起点に他の要素を検索することも出来ます。

```java
// 親要素を検索
$("td").parent();
// 上方向に一番近い要素を検索
$("td").closest("tr");
// 子要素を検索
$("div").find(By.name("q"));
```

## 画面の操作
### 基本的な画面操作
取得したSelenideElementのメソッドで画面を操作していきます。

```java
// テキストを上書き（val(String)と等価です）
$("#txt").setValue(String);

// テキストに追記
$("#txt").append(String);

// クリック
$("#btn").click();

// ダブルクリック
$("#btn").doubleClick();

// 右クリック
$("div").contextClick();

// ドラッグ&ドロップ（ドロップ先はセレクタで指定）
$("#id1").dragAndDropTo("#id2");
$("#id1").dragAndDropTo($("#id2"));

// マウスホバー
$("div").hover();

// ENTERキー
$("#txt").presEnter();

// ESCキー
$("div").presEscape();

// TABキー
$("#txt").presTab();

// Selectボックス選択
$("#select").selectOption(0); // indexで選択
$("#select").selectOption("北海道"); // textを指定して選択（完全一致）
$("#select").selectOptionContainingText("山"); // textを指定して選択（部分一致） v4.2から
$("#select").selectOptionByValue("01"); // valueを指定して選択

// ラジオボタン選択
$("id").selectRadio("abc"); // valueを指定して選択

// チェックボックス選択
$("id").setSelected(boolean);

// 要素までスクロール
$("id").scrollTo();
```
### アップロード
と言っても実際にファイルをアップロードする訳ではなく、`<input type="file">`の項目にパスを設定するだけ。
複数ファイルを指定した場合、内部的にDOMを操作して同じnameのfileフィールドのコピーを作成します。

```java
$("#file").uploadFile(new File("test.csv"));
$("#file").uploadFromClassPath("files/test.csv");
```

### ダウンロード
hrefで指定されたURLを内部的に呼び出してFileを返してくれます。
jsが噛んでいたり、ちょっと凝ったダウンロードは自前で書かないとダメそうです。

```java
File csv = $(".download-button").download();
```

## 値の取得
見れば分かると思いますこんな感じです。

```java
$("#id").getValue(); //val()と等価
$("#id").getText(); //text()と等価
$("#id").name();
$("a").attr("href");
$("#id").innertText();
$("#id").innerHtml();
$("#id").exists();
$("#id").isImage();
$("#id").getSelectedOption();
$("#id").getSelectedValue();
$("#id").getSelectedText();

```

## Assertion
Assertionメソッドも用意されています。

```java
$("div").shouldHave(text("Hello world!"));
```

SelenideElementとElementsCollectionのshouldで始まるメソッド群がそれです。
（沢山ありますが、shouldとshouldBe、shouldHaveは等価。shouldNotとshouldNotBe、shouldNotHaveは等価だったりします）


デフォルトで4秒間100ミリ秒間隔でリトライしてくれますので、Ajaxを考慮して明示的にwaitを書く必要がありません。
恐らくこれがSelenideを使う最大の利点じゃないかと。
何秒間でタイムアウトさせるかは、`-Dselenide.timeout=8000`のように、ポーリングする間隔は`-Dselenide.pollingInterval=200`のように変更可能です。

また、Conditionクラスにはshouldに渡すMatcherを生成するメソッドやフィールドが用意されています。
意味は名前から想像が付くと思います。

- id(String)
- cssClass(String)
- text(String)
- textCaseSensitive(String)
- exactText(String)
- exactTextCaseSensitive(String)
- attribute(String, String)
- attribute(String)
- value(String)
- type(String)
- name(String)
- visible #appear,appearsは等価
- hidden  #disappear,disappearsは等価
- checked
- selected
- enable
- disabled
- readonly
- empty
- exist
- focused

## Assertionの結果表示
例えば、`$("#gs_st0").should(checked)`と書いた場合、4秒待ってもcheckedにならないと、こんな感じにエラーが表示されます。
実際のHTMLが表示されるのでエラーが分かり易いと思います。
更にデフォルトではエラー時のスクリーンショットとHTMLファイルも保存してくれます。

```
Element should checked {#gs_st0}
Element: '<div class="gsst_b sbib_c" dir="ltr" id="gs_st0"></div>'
Actual value: false
```

# Pageオブジェクト
Pageオブジェクトが標準でサポートされています。
サンプルコードをPageオブジェクトで書き変えてみます（公式ページのまんまです）

```java
public class GoogleSearchPage {
  public GoogleResultsPage search(String query) {
    $(By.name("q")).setValue(query).pressEnter();
    return page(GoogleResultsPage.class);
  }
}

public class GoogleResultsPage {
  public ElementsCollection results() {
    return $$("#ires li.g");
  }
}
```

```java
GoogleSearchPage searchPage = open("/login", GoogleSearchPage.class);
GoogleResultsPage resultsPage = searchPage.search("selenide");
resultsPage.results().shouldHave(size(10));
resultsPage.results().get(0).shouldHave(text("Selenide: Concise UI Tests in Java"));
```

`Selenide.open`で最初のPageオブジェクトを返した後は、`Selenide.page()`で次のPageオブジェクト生成してバトンタッチしていくイメージです。
Pageオブジェクトのメリットですが、画面操作とテストが分離され、よりテストに集中できるようになりました。

## Page Objectのフィールドにエレメントを定義する

[公式サイト](http://selenide.org/documentation/page-objects.html)では "Classic Page Objectはいくつかdisadvantagesがある" と嫌っているようですが、@FindByアノテーションを使うことでクラスのフィールドにSelenideElement、ElementsCollectionを持てます。（disadvantagesな理由はよく分からない）

```java
@FindBy(css = ".login")
private SelenideElement loginLink1;
```

4.2.1からは下記のようにアノテーションを使わない方法もサポートされました。

```java
private SelenideElement loginLink2 = $(byClassName("login"));
```

# 小技など
## カスタムMatcher
Matcherは自作できるので、足りなければ自分で書きましょう。

```java
public static Condition css(String key, String value) {
    return new Condition("css") {
        @Override
        public boolean apply(WebElement element) {
            return value.equalsIgnoreCase(element.getCssValue(key));
        }
    };
}
```

## Javascriptを実行する
どうしてもDOMをゴリゴリいじりたい場合はJavascriptで書くことも可能。

```java
Selenide.executeJavaScript("console.log('hoge')");
```

## テストが終わった後もブラウザを開いたままにする
試行錯誤している段階ではブラウザが開いたままでいて欲しいので。

```java
Configuration.holdBrowserOpen = true;
```

## スクリーショットを撮る
デフォルトではpng画像とその時のHTMLファイルが保存される。

```java
// 保存場所を設定
Configuration.reportsFolder = "test-result/reports";
// -Dselenide.reports=test-result/reportsでも設定可能

// 画面全体を撮りたい場合（my_file_name.pngとmy_file_name.htmlが保存される）
Selenide.screenshot("my_file_name");
// 特定の要素だけ撮りたい場合
$("#hoge").screenshot();
```

## 値を素早く入力したい
デフォルトだと1文字ずつsendKeyする感じで遅いですが、このオプションをONにすると一発で文字が入力されるのでテストが速くなるとのことです。（自分のシステムでは文字入力が少なくて体感できませんでしたが）

```java
Configuration.fastSetValue = true;
````
## Sizzleを使った要素検索に切り替える

`Configuration.selectorMode = Configuration.SelectorMode.Sizzle`と指定すると、セレクタの検索がSizzleに置き換わり `:not()`,`:header`などデフォルトでは使えないCSS3のセレクタが使えるようになる。

## Selenideではどうにも出来ない時
WebDriverRunner.getWebDriver()でWebDriverが取得できるので自前で頑張りましょう。
自分は微妙な位置にドロップしたい場合にどうしようもなく自前でゴリゴリ書きました。

```java
// ドロップする位置を少しずらす量を指定
int offset = 5;
// D&D部品の高さを求め、offset の分だけずらした位置でドロップする
int height = $$("#list").get(index).getSize().getHeight();
new Actions(getWebDriver())
        .clickAndHold($$("#list").get(index))
        .moveByOffset(0, height + offset)
        .release()
        .perform();
```

## Slenide Profilerで性能測定

1つ1つの操作にかかった時間をログに出力できる。

```
1 07, 2017 11:23:58 午前 com.codeborne.selenide.logevents.SimpleReport finish
情報: Report for googleSearch(com.example.GoogleTest)
+--------------------+----------------------------------------------------------------------+----------+----------+
|Element             |Subject                                                               |Status    |ms.       |
+--------------------+----------------------------------------------------------------------+----------+----------+
|open                |http://www.google.com/                                                |PASS      |10683     |
|By.name: q          |val(selenide)                                                         |PASS      |371       |
|By.name: q          |press enter()                                                         |PASS      |357       |
|#ires .g            |should have(size(10))                                                 |PASS      |1341      |
|#ires .g[0]         |should have(text 'Selenide: concise UI tests in Java')                |PASS      |201       |
+--------------------+----------------------------------------------------------------------+----------+----------+
```
JUnitの場合は下記のようにルールを追加するだけ。やり方は違うけどTestNGでも同じようにログ出力できるらしい。

```java
@Rule
public TestRule report = new TextReport();
```

# まとめ
E2Eのテスト自体今年から始めたSeleniumの使用経験がない初心者でしたが、特にハマることもなくテストが書けています。
Selenideはテストに専念させてくれる素晴らしいツールだと思います。