Seleniumアレルギーのための処方箋

  • 630
    Like
  • 1
    Comment

何年も前、SeleniumやWebDriverの話で盛り上がった記憶があります。ただ、その当時はまだRailsなどバックエンド中心の文脈でした。今、フロントエンドに軸足が移る中、ブラウザテストの状況はどうなったのでしょう?

不思議なことに、フロントエンド界隈でそれほど話題に上がって来ないですよね (私の周りだけ?)。結構大事なのに。実は皆さん、「Seleniumアレルギー」なんじゃないですか? 公式サイトに漂う ゼロ年代感(下図)。Javaへの躊躇、「めんどくさい」と聞かされ続けた過去、無意識に避けてしまうのがSeleniumです。

ただ、フロントエンドの文脈でこそ、ブラウザテストは重要度を増しています。そこで「Selenium触りたくない病」の筆者が、 四苦八苦した背景 と、2016年だからこそ 見えてきた落とし所 を書いてみたいと思います。

註: 思ったより長文になってしまいました。先に結論をという方は「結論」の節へどうぞ。

想定読者:

  • そういえば、Selenium(↓)って聞いたことある人
  • フロントエンドでブラウザテストどうしようか悩んでる人
  • Seleniumアレルギーの人

1470470325-81396F0E-49A1-42FF-8973-3A0F2814A9CF.png

フロントエンドにはどんなテストが必要?

以下、大雑把にユニットテスト、UIテスト、E2Eテストに分けて、比較的よく聞くツールを並べてみます。先週書いた「ブラウザテストツール総まとめ・2016年夏版」もご参考まで。

タイプ 一般 AngularJS系 React系 備考
ユニットテスト Mocha, Tape, Ava... Jasmine Jest? 正直なんでもよし
UIテスト Karma Karma Enzyme 選択肢が乏しい
E2Eテスト
(ブラウザテスト)
Nightwatch
WebdriverIO
Codecept
Nightmare ほか
Protractor 一般に準ず 基本、Selenium系

フロントエンドで「UIテスト」は特に重要です。E2Eとユニットテストの中間で、UIコンポーネントのみを扱うという意味では「ユニットテスト」の一種ですが、実際のブラウザやDOMの挙動をエミュレートしたもの(JSDOM等)を使うため、ツールとしてはE2Eテストに近いです。

では、UI(コンポーネント)のテストまではさておき、その先のブラウザテストをどうするか...、この中に選択肢は存在するのか、考えていきたいと思います。

註: UIテストまで含めて「ユニットテスト」と呼んでいる場合があります。テストの種類は多岐に渡りますが、文脈によって呼び名が変わるので注意が必要。

E2Eテストの意味

タイプ 目的 実行環境 コードカバレッジ
ユニットテスト 関数の挙動(入力と出力)を確認する Node 取る
UIテスト UIコンポーネントの挙動を確認する ブラウザ
DOMエミュレーション
取る
E2Eテスト
(ブラウザテスト)
??? ブラウザ 取らない

表中の「E2Eの目的」のところ「???」としました。E2Eの本来の意味であれば、End to endなので、サーバサイドの奥底から、UIまで通したテストを指します。しかし、SPAの場合、バックエンドはAPIでしかないため、モックで済ませるケースもあるでしょう。その場合、正確には「フロントエンド部分の結合テスト」ですが、境界は割と曖昧です。その分、何を目的とするのか、意識合わせが必要かもしれません。

コードカバレッジ

旧来のバックエンド中心の文脈では、E2Eテストといえばコードカバレッジを基本とりませんでした。今も「統合テストでカバレッジを取るのはナンセンスだ」と思う諸兄が多いようです。ただ、SPA(Single Page Application)だと、カバレッジ取るのは不可能ではないし、必要な局面もあるでしょう。認識は変わっていくかもしれません。

ヘッドレスブラウザの変遷

かつて、ブラウザといえばChromeやSafari、Firefox、IEといった実際にユーザが触れるものだけを指す言葉でした。そんな状況が変わるのは2012年前後です。「ヘッドレスブラウザ」がテストで使われるようになってきたのです。

PhantomJSは現在もっとも利用されるヘッドレスブラウザです。リリースされた2011年頃の時点では「スクレーピングツール」という認識が主でしたが、2012年のうちにはブラウザと肩を並べるレベルまで実装が進みます。そして、GhostDriverの登場で、PhantomJSでもWebDriverが使えるようになります。2013年3月、v1.9のリリースでPhantomJSは一旦の完成をみます。

註: Karmaが作られたのもこの頃です。Karmaは主にWebDriverに対応した各種のランチャーを持ち、Selenium Standalone ServerやPhantomJSを実行環境に「ユニットテスト」できる、画期的なツールとなりました。

時代遅れになるPhantomJS

Firefox互換のSlimerJSを除き、しばらく新たなヘッドレスブラウザが登場することはありませんでしたが、Webの状況は大きく変わっていきます。フロントエンドの台頭と、ES6の波です。そんな中、PhantomJSはv1.9で長らく足踏みを強いられます。SafariとChromeでWebKitは袂を分かち、競って新機能が実装され、HTML5が標準になり、ES6(ES2015)が勧告されるに至りました。当然、ブラウザの中身は2013年と2015年では大きく異なりますが、PhantomJSの中身は2013年当時のままだったのです。

しびれを切らした人々は、Reactを中心にJSDOMを本格的にテストで用い始めます。ReactはサーバサイドレンダリングでJSDOMをすでに使用していたので、生成とテストという車の両輪でもあったのです。

その後、同じくしびれを切らした別の人々はElectronが、Chromeの代わりに使えることに気づきます。NightmareはもともとPhantomJSのラッパーだったのですが、最近エンジンをElectronに換えて独自性を出してきました。現在は4つのヘッドレスブラウザ(的なものも含む)が利用されています。

プロダクト 特徴 WebDriver 安心ポイント 不安ポイント
PhantomJS WebKit互換 GhostDriver ほとんどSafari 「どんだけPolyfillさせるんだ!」
「ブラウザエンジンが旧い...」
※上記はv1.9の当時の話です
SlimerJS Firefox互換 ない ? 「ユーザがいない...」
Nightmare 中身はElectron ない ほぼ最新版のChrome 「テストを他のブラウザに持っていけない」
JSDOM Node環境でブラウザを
エミュレーション
ない コードがクリーン 「本当にちゃんとブラウザしてる?」
「はてしないマッチポンプ感」

時代を追いかけるPhantomJS

2015年の暮れまで「PhantomJSがつらい」がJSDOMほかの原動力になっていたのは否めません。しかし、そんなPhantomJSもついに2016年1月v2.1を迎えます。実にほぼ3年ぶりのアップデートでした。

実際には2015年にv2.0がリリースされていたのですがβ版扱いだったため、v2.1が実質的には2系で最初のリリースとなります。 なお、v2.0からはWebKitではなく、Chromiumベースに変更されました。

年/月 PhantomJS エンジン 備考 同時期のSafari 同時期のChrome
2013/03 v1.9 (Qt4系) 534.34 Safari5系に相当 6.0 (536.25) 26
2015/01 v2.0 (Qt5.4) 538.1 Qt5系に切り替わる 8.0 (538.35.8) 40
2016/01 v2.1 (Qt5.5) 538.1 9.0.3 (601.4.4) 48

現状でも、最新版のSafariやChromeとの開きがあり、「時代に追いついた」とは言い難いところがあります。

註: ちなみに、Electron v1.3.2 はすでにChromium v52 相当です。
註: 初出時「v2.0からQt WebEngineに切り替えた」前提で書いていたのですが、勘違いです。すみません。すくなくともv2.5でもQt WebKitを使っています。

クロスブラウザテストは必要?

サーバサイドとは異なり、フロントエンドの開発はライブリロードが基本です。最近では更に進んでモジュール単位で書き換える"hot reloading"も主流になりつつあります。つまり、自分のコンピュータの環境で常にテストしながら動かしているようなものです。だからと言って自動テストなしで済むという話ではありませんが、サーバサイドの感覚と大きく乖離している点は留意するべきでしょう。

しかし、これらも些細なことです。すでにテストの軸足はモバイルに移ってしまいました。モバイルファーストなら、テストもモバイルファーストであるべきです。モバイルのテストをデスクトップでやりますか? それもノーです。もう専門業者に任せるほかありません。結論としては、こうなるでしょうか。

  • ローカルでのクロスブラウザテスト: 不要
  • クラウドでのクロスブラウザテスト: 必要

註: これは自動テストにおいての話です。筆者もデザイン段階ではローカルで複数のブラウザ(と端末)で確認しながら進めています。参考・BrowserSync

テスティングクラウド

各社で、基本的なブラウザの種類・バージョンに大きな違いはありませんが、APIの充実度、サポート体制などは異なります。自動テストが可能なプランの下限も、サービスによって開きがあります(下記)。

高いイメージが先行して、安価に使い始められるTestingBotなどの存在は、あまり知られていないのかもしれませんね。割と、個人で契約しても負担にならない範囲です。

1470470576-F067D2E8-7422-4FA0-9C78-5372A8F673F1.png

クラウドはタダじゃない

先ほど触れたように、ローカルでのクロスブラウザテストは不要に(不可能に)なりました。代わりに求められているのが「仮の」テストです。次の比較表を見ると明らかなように、クラウドテストにはコストがかかります。

タイプ かかる時間 かかる費用
ローカルテスト 数十秒〜3分程度 無料
クラウドテスト 数分から10分程度 約50円/回 (10分かかるとして)

上記は、それほど規模がなく、ブラウザの種類も少なめで実行した場合です。大規模なアプリケーションであれば、テストに1時間かかったとしても不思議ではありません。毎回のクラウドテストを実行するのは、開発効率を著しく下げてしまいます。

そこで事前に行うのが、ブラウザを限定した仮テストです。ローカルで仮テストにパスしたものだけクラウドに送れば、時間も費用もコストを最小限に抑えられます。

註: 「仮テスト」という呼び名は一般的ではありません。便宜上そう書いています。何か丁度良い用語があると良いのですが...。

仮テストはヘッドレスブラウザで

ただ、ローカルにインストールされているブラウザは、人それぞれです。Operaがメインの人もいれば、Chromiumの最新版という人もいるでしょう。しかし、テスト環境に組み込むにはプロジェクト内で統一しなくてはなりません。一番の候補はヘッドレスブラウザです。KarmaとWebdriverIOは、ローカル環境でPhantomJSが起動していれば、その中でテストを実施できます。

プロダクト ローカルテスト クロスブラウザテスト (クラウドで)
WebdriverIO PhantomJSなど Chrome, Firefox, Safari, IE, iOS, ...
Karma PhantomJSなど Chrome, Firefox, Safari, IE, iOS, ...
Enzyme JSDOM できない
Nightmare Electron できない

表にはWebdriverIOを挙げましたが、ほかのE2Eテストツールでも同様です。EnzymeとNightmareはテスト対象が固定で、クロスブラウザテストを実行できない点、注意が必要です。Electronに期待しつつも、テスト環境としてはPhantomJSが無難な印象です。

註: Nightmareは筆者も便利に使っていますが、手軽な反面、クロスブラウザテストが必要な段階になると、テストコードをごそっと書き直す必要が生じます。(それでも構わない規模感で使うなら全然ありです)

Seleniumふたたび

ここまで、Seleniumに触れず、フロントエンド視点でボトムアップに話を進めてきました。が、そろそろ触れざるもえません。そもそも、Seleniumとはなんなのでしょう? Seleniumのサイトのトップには

Selenium automates browsers. That's it!

とフレンドリーに書いてありますが、なんのことやら。全然フレンドリーじゃありません。結論から言うと、下図に示す ブラウザ自動化のためのエコシステム全体 を称して「Selenium」と呼び習わしています。(図中の左側は、Node関連のものに限っています)

1470459339-DD76A2D1-9973-4D3F-AC76-98C8F77C8643.png

次の4層と、場合によってはWebDriver クライアントを抽象化するライブラリ(Protractorほか)で構成されます。

  • WebDriver クライアント
  • Remote WebDriver
  • WebDriver
  • ブラウザ

1台だけで実行する場合、Remote WebDriverは必ずしも必要なく、WebDriverクライアントは直接WebDriverに接続することも可能です。ただ、クライアントライブラリは基本Remote WebDriverとの接続を前提に作られるため、実際のテストではSelenium Standalone Serverを必要とすることが多いです。(これが混乱のもと...)

註: たとえば、Protractorでも直接接続できます。WebdriverIOでは特に指定しなくても、ローカルでWebDriverが動いている場合、直接接続が可能です。

SeleniumからWebDriverへ

公式サイトによれば、Seleniumの歴史は2004年にまで遡ります。Selenium-IDEを開発したのが日本人だったこともあり大きな話題になりました。簡単に歴史を追うと...

  • Selenium v1: 2004年〜 ブラウザに操作用のJavaScriptをインジェクション。ブラウザ自動化の必要性を世に広めた。
  • Selenium v2: 2007年〜 WebDriverとしての標準化と、ブラウザ側のネイティブ対応が始まる。
  • Selenium v3: 2016年(予定) 現在ベータ版。v1時代の遺物であるRemote Control APIs が廃止されます。

SeleniumとWebDriverの関係は、ホッチキス(商標)とステープラ(一般名詞)のようなものです。固有のプロダクトとして始まったSeleniumは、WebDriverという一般名詞になりW3Cで標準化されようとしています(まだドラフト)。

註: 狭義のWebDriverは旧Json Wire Protocolを指している場合もあります。

全主要ブラウザがWebDriverに対応 (2016年)

近年、"Next IE"などと揶揄されてきたSafariでしたが、次期OSにバンドルされるSafari10は、ES6 100%対応とともに、WebDriverの公式対応が入ります。

これで、Safariを最後に、全主要ブラウザのWebDriver対応が完了しました。ちなみに、Microsoftは2015年にすでにEdgeのWebDriver対応を発表済みです。

ブラウザ WebDriver 備考
Chrome ChromeDriver C++とPythonで書かれている
Firefox Marionette FirfoxDriverの後継
Edge Microsoft WebDriver
Safari SafariDriver
PhantomJS GhostDriver Java + JavaScript すでに開発停止
メンテナ募集中

落としどころはどこに?

さて、ここまでが長い長い前置き、背景説明です。ここまでで見えてきたのは、

  • モバイルファーストな今、ローカルでクロスブラウザテストする必要性は低い
  • ローカルでは、ヘッドレスブラウザを使うのが良さそう
  • 各社のブラウザで、WebDriverの環境が整った

という点でした。合わせて考えたいのが次の点です。

  1. テストフレームワークと疎結合であること
  2. テストコードを短く書ける
  3. 同じコードで、ローカルテストとクロスブラウザテストができる
  4. 依存としがらみが少ない (アプリケーションの層が少ない)

これを踏まえて「ブラウザテストの落とし所」を探ります。候補はこのあたり。

プロダクト 1.疎結合 2.テストが短い 3.クロスブラウザ 4.依存としがらみ
Nightwatch × なし
selenium-webdriver × なし
Protractor selenium-webdriver
Angular
WebdriverIO なし
Chimp WebdriverIO
Codecept × WebdriverIO
Nightmare × なし

独自路線のNightwatch

Nightwatchは、テストフレームワーク、アサーションライブラリが最初から組み込まれた形で提供されます。個人的に気になってしまうのが「操作とアサーションが分離されない」ところ。

module.exports = {
  'Demo test Google' : function (client) {
    client
      .url('http://www.google.com')
      .waitForElementVisible('body', 1000)
      .assert.title('Google')
      .assert.visible('input[type=text]')
      .setValue('input[type=text]', 'rembrandt van rijn')
      .waitForElementVisible('button[name=btnG]', 1000)
      .click('button[name=btnG]')
      .pause(1000)
      .assert.containsText('ol#rso li:first-child',
        'Rembrandt - Wikipedia')
      .end()
  }
}

Protractorもクセあり

Angularのしがらみとは別に、Protractorの好き嫌いが分かれるのは、グローバルが多めな点です。browserのほか、by()element()など。短く書けるという点では悪くないのですが...。もし、Angularのプロジェクトであれば、もちろん悩まずProtractorでOK。

describe('angularjs homepage todo list', function() {
  it('should add a todo', function() {
    browser.get('https://angularjs.org')

    element(by.model('todoList.todoText')).sendKeys('write first protractor test')
    element(by.css('[value="add"]')).click()

    var todoList = element.all(by.repeater('todo in todoList.todos'));
    expect(todoList.count()).toEqual(3)
  })
})

Nightmare、Codecept、selenium-webdriver

Nightmareの書き方は非常にシンプルです。ブラウザ内でJavaScriptを実行できるのも便利です。ただ、クロスブラウザを考慮するなら対象から外さざるを得ません。

Codeceptも独特な書き方をします。一種のDSLで、Cucumberの流れに近いものがあります。問題は、これを受け入れられるかどうか...

Feature('CodeceptJS Demonstration')

Scenario('submit form successfully', I => {
  I.amOnPage('/documentation')
  I.fillField('Email', 'hello@world.com')
  I.fillField('Password', '123456')
  I.checkOption('Active')
  I.checkOption('Male')
  I.click('Create User')
  I.see('User is valid')
  I.dontSeeInCurrentUrl('/documentation')
})

selenium-webdriverはどうでしょう? ちょっと触ると分かりますが、コードが非常に冗長で、素で書くものではないです。

driver.get('http://www.google.com')
driver.findElement(webdriver.By.id('q')).sendKeys('webdriver')
driver.findElement(webdriver.By.id('btnG')).click()

WebdriverIO (とChimp)

今しがたの例を、WebdriverIOで書き直すと、こうなります。

browser
  .url('http://google.com')
  .setValue('#q', 'webdriver')
  .click('#btnG')

圧倒的に見やすいです。Mochaでのテストは次のとおり。クセのあるライブラリを見続けた後だと 素直なAPI にほっとします。(プロジェクト自体もSaucelabsの中の人が率いていて安心感があります)

const assert = require('assert')

describe('First Test Group', () => {
  it('gets the title of MDN toppage', () => {
    const title = browser.url('https://developer.mozilla.org/en-US/').getTitle()
    assert.equal(title, 'Mozilla Developer Network')
  })
})

ChimpはWebdriverIOのお膳立ての面倒を避けるために作られました。実際、上記と同じように書けます。しかし、WebdriverIOがバージョンアップしてきた結果、Chimpの機能はほとんど本家に取り込まれてしまいました。もう、あえてChimpを使う必要性はありません。

このWebdriverIOですが、今年になって出たv4ではSynchronous Modeがデフォルトになりました。内部的な実装は、すべてPromiseを使った非同期なコードです。その上で、Fiberを使ってすべて同期的な関数に変換するという大技をかましています。

const title = await browser.getTitle() // v3での書き方
const title = browser.getTitle() // v4での書き方

一見、JavaScript界隈の流れに逆らうかのようですが、よく考えられた末での決断がうかがわれます。E2Eテストの場合、ほぼすべてのコマンドは非同期です。そのたびにyield(あるいはawait)するのは煩わしいだけ。はじめから同期関数として実装されていればいいのに! が叶ったのがv4です。

結論

  • 結論、 WebdriverIO ただし、AngularならProtractor
  • ローカルではPhantomJSのみでテスト
  • クロスブラウザテストはクラウドで、モバイルファースト!

基本的には気に入ったライブラリを使えばよいと思います。筆者の場合は、違和感を覚えるものを除いていくとWebdriverIOが残りました。

Selenium Serverはクラウドでのみ実行されるので、ユーザはほとんどJavaを意識する必要がありません。フロントエンド界隈では比較的大きなメリットとなるように思います。(実は、一部、残ってしまうJavaのコードもあるのですが...)

註: ちなみに、Javaのコードが残ってしまうのは次の2点です。

  • PhantomJSにバンドルされたGhostDriver
  • テスティングクラウドを使う際のトンネルアプリケーション

ブラウザテストの構成

登場人物を図にまとめてみます。シンプルな構成になりました。

1470467776-81D4B4E7-F7D7-4E28-B2C5-6C23FBD6CE4F.png

もし、CIサービスを使っていれば、次のようになります。CIサーバでのみクロスブラウザテストを実施するようにすれば、テストのストレスはかなり軽減されるはずです。

1470467746-BD952970-77F2-48BD-85AC-45E375AE47DA.png

註: お値段的に手頃なTestingBotで説明していますが、SaucelabsでもBrowserStackでも構いません。お財布と必要なAPIとの折り合いで決めてください。上図のバージョン番号は適当です。

構成例

ここに置きます。解説はまた後日。

最後に

以上、Seleniumを2016年現在の文脈で説明し直して、Selenium Standalone Serverを立ち上げずに済ます構成に落とし込むというお話(処方箋)でした。おつかれさまです。

長い旅でしたが、答えにたどり着いてほっとしました。「PhantomJSをElectronに替えたい」という思いは、ひとまず封印して、次までの宿題にしたいと思います。
とりあえず、WebdriverIOいいですよ! :smile: