PythonのSeleniumラッパーSeleneの使い方

## 概要

JavaのSeleniumWebDriverラッパーとしてはSelenideが有名ですが、PythonではSeleneがメジャーなようです。

公式のドキュメント等見てもイマイチわからないところが多かったので、自分で調べてみたついでにわかったことを残しておきます。

※色々調べてみた感想としては、Seleneの中身読むのが一番わかりやすかったです。

## そもそもSelenとは

- Effective web test automation toolである
  - 簡潔なAPI
  - 要素等の検索を待つ
  - アサートを待つ
  - 動的な要素にも対応
- UIテストのロジックを自動化するもの


## 準備

### 手元の環境

- Windows10Pro
- Python 3.7.2
- Selenium WebDriver 3.141.0
- pytest 5.0.0
- GoogleChrome 77
- FireFox 70

### 環境構築

SeleniumWebDriverとPythonで自動テストを書ける状態に加えて、Seleneのインストールが必要です。

インストール手順などは公式に書いてあります。

[yashaka/selene: Concise UI tests in Python + Ajax support + PageObjects](https://github.com/yashaka/selene)

が、バージョンごとに複数手順があるようなので、今回は

> latest published pre-release version (currently this is recommended option unless selene 1.0 will be released):

と書いてある最新のプレリリース版を使おうと思います。

``` bash
$ pip install selene --pre
```

## 使い方

### 画面の要素を取得する

`browser.element('#new-todo')`のように書くことができますが、基本はより短い`s`を使います。

`find_element_by_id("hoge")`と書くかわりに、

- `s('#hoge')`
- `s(by.id('hoge'))`

のいずれかの書き方ができます。

一つめのほうは、sの引数にそのままCSSセレクタを書くパターン。もうひとつのほうは、byを使ってidなりxpathなりを使う、生のWebDriverに近いパターン。

いずれにせよ、行は減らないものの文字数はだいぶ減っています。ただ、読んで理解しやすいかというと怪しいかもしれませんね。

sは"search element"のsだそうです。

`find_elements_by_hoge`を行いたいときには、`ss`を使います。

``` Python
ss('#todo-list>li')
```

こちらは、`browser.all('#todo-list>li')`とも書けますが、やはり短い`ss`のほうを公式も勧めています。


### スクリーンショット取得

生のWebDriverだと`save_screenshot()`メソッドを使うところ、Seleneの場合は`take_screenshot()`になるようです。

`take_screenshot()`はパスとファイル名の二つの引数をとり、どちらも書かなくても動くようにはなっています。

第一引数で指定するパスはスクリーンショットの保存先のパス、第二引数で指定するファイル名はスクリーンショットのファイル名です。

たとえば、

``` Python
browser.take_screenshot(".\\capture")
```

とだけ書いた場合。この場合は、スクリプトと同階層にcaptureというフォルダができ、そこにスクリーンショットがどんどん追加されていきます。
このときのファイル名は過去のものと重複しないようにSeleneが付けてくれます。`screen_1571995717156.png`のような名前になります。

引数に何も指定しないと、デフォルトのレポート保存先に"screenshots"というフォルダを作って保存されます。

私の手元の例は↓です。

> C:\Users\yoshiki.itou\\.selene\screenshots\1572231343309\screen_1572231343310.png

毎回保存先を指定するのが面倒な場合は、引数指定なしの場合の保存先を設定変更することができます。

``` Python
config.reports_folder = ".//screenshot/"
```

と書くと、実行したテストスクリプトと同じ階層にscreenshotsというフォルダを作り、その中にスクリーンショットを保存していきます。

### タイムアウトの設定

例えば
``` python
s("#new-todo").should_be(enabled)
```

とだけ書いた場合、デフォルトでタイムアウトは4秒。

``` python
s("#new-todo").should_be(enabled, timeout=10)
```

と書けば、この場合のタイムアウトが10秒になります。

もしくは

``` python
config.timeout = 10
```

と書いておけば、以降のタイムアウトが10秒になります。

### 実行するブラウザの指定

SeleneはAutomatic driver managementというのをやってくれるので、自分でダウンロードしてきたhogedriver.exeのパスを指定or環境変数PATHを通して・・・という作業をしなくて済むようになっています。

このへんは[SergeyPirogov/webdriver_manager](https://github.com/SergeyPirogov/webdriver_manager)を利用しているようです。

デフォルトではChromeが起動するようになっているようですが、明示的に指定する場合は以下のように書きます。

``` Python
from selene import config
from selene.browsers import BrowserName

config.browser_name = BrowserName.CHROME
```

FireFoxで動かしたい場合、CHROMEのところを単純に
``` Python
config.browser_name = BrowserName.FIREFOX
```

にすればうまくいくだろうと思ったのですが、私の環境ではエラーで動作せず。

中身見てみたところ、どうも

``` Python
config.browser_name = BrowserName.MARIONETTE
```

だと動作するようでした。

※ここについては、みんなそうだという自信はないので、FIREFOXでダメなときはお試しください。

[selene/browsers.py at master · yashaka/selene](https://github.com/yashaka/selene/blob/master/selene/browsers.py)あたりを見てみても、FIREFOX自体は設定できるので、ちょっと謎です。わかる方教えてください。

### アサート

アサートに使えるものは

- `should`
- `assure`
- `should_be`
- `should_have`
- `should_not`
- `assure_not`
- `should_not_be`
- `should_not_have`

がありますが、内部的には

- `should`
- `should_not`

の2つで、他は上記いずれかのエイリアスです。

アサーションのために`should`あるいはそのエイリアスを使うにあたって、PASS時は特に困ることはなさそうです。

一方FAIL時は多少厄介で、TimeOutExceptionが出てきてしまいました。

たとえば、以下がFAIL時の表示の例です。画面に51000と表示されるべきところ、わざと「41000と表示されるか」をテストして失敗させています。

コード(抜粋)

``` Python
s(by.id("price")).should(have.text('41000'))
```

結果(抜粋)

``` 
E           selenium.common.exceptions.TimeoutException: Message:
E                       failed while waiting 4 seconds
E                       to assert Text
E                       for first_by('id', 'price')
E
E                       reason: ConditionMismatchException: condition did not match
E                               expected: 41000
E                                 actual: 51000
E                       screenshot: file://.//screenshot/screen_1572244618378.png

..\..\..\..\appdata\local\programs\python\python37\lib\site-packages\selene\elements.py:232: TimeoutException
```

ConditionMismatchExceptionなのでそこ見ればわかるだろと言われればそうなのですが、エラーメッセージの最後の最後でTimeoutExceptionで出てきてしまうのがうーん。。

とはいえ、比較のときには標準のassert使う、だとラッパーライブラリのうまみ半減なので、難しいところですね。

また、標準のassertだと条件式がFalseのときだけ表示するメッセージを第三引数に書いておけますが、Seleneのアサートだとそれが無さそうです。

## 参考

- 公式
  - [yashaka/selene: Concise UI tests in Python + Ajax support + PageObjects](https://github.com/yashaka/selene)
- 作者による紹介プレゼン資料
  - [Selenide alternative in Python](https://www.slideshare.net/yashaka/selenide-alternative-in-python-introducing-selene-seleniumcamp-2016?ref=https://seleniumcamp.com/talk/selenide-alternative-in-python-introducing-selene/)
  - こっちのほうがGitHubのREADMEよりわかりやすい