agri-note inside

ウォーターセル株式会社 スマート農業システム開発部のブログです。

WebdriverIO + AppiumでCordova製iOSアプリをテストする(前編)

こんにちは、開発部の中川@Nkznです。

Webサービスやモバイルアプリの品質保証を実施するにあたり、機種依存の不具合を見つけるために、複数のブラウザや複数のモバイルデバイスでテストを行うことがあります。弊社でも、品質管理チームが手作業で複数ブラウザ・複数デバイスへのテストに取り組んできました。

しかしながら、事業の拡大や機能の複雑化に伴い、テストにかかる時間も大きくなってきています。人を増やすという手もありますが、まずはUIテスト(主に回帰テスト)の自動化をしてみようということになりました。

そんなわけで、品質管理チームの主導で、UIテストのツールを調査しています。

Cordova製のiOSアプリをテストする

弊社ではCordova製のiOSアプリをメンテナンスしています。こちらも自動UIテストの対象になりますが、さて、どうやってWebViewにアクセスすればよいのでしょうか。

XCUITest をSwiftでゴリゴリと弄って、WebViewのインスタンスを直接触ればどうにかなりそうな気もしていますが、できれば品質管理チームの学習コストを減らしたい気持ちもあります。 Write OnceLearn Once くらいの学習コストで済みそうなソリューションが欲しいところです。

AppiumでWebViewがテストできる

いろいろ調べてみたところ、Appiumを切り口にするとよさそうなことがわかってきました。

Appiumといえば、ネイティブUI向けの自動UIテストのツールとして広く知られていますが、どうやらWebViewへの対応も進んでいるようです。まずは通常のAppiumの使い方について確認してから、WebViewの扱いについてお話ししましょう。

WebDriverの話

Appiumについて理解する上で避けて通れないのが、UI操作を自動化するための標準仕様であるWebDriverの話題です。

WebDriverは、ブラウザのUI操作を自動化するためのツールとして生まれたSeleniumの一部が標準化される形で、W3Cにより策定された仕様です。

w3c.github.io

SeleniumでのWebDriverドキュメントのひとつであるUnderstanding the components :: Documentation for Seleniumによると、いくつかの運用方法があるようですが、ここでは本記事と関係の深い Remote WebDriver についてのみ言及します。 Remote WebDriver の構成を取った場合、WebDriverの利用イメージは次の図のような形になるそうです。

f:id:Nkzn:20191122175221p:plain:w480
Remote WebDriver構成(公式ドキュメントより抜粋)

図の右下にある Remote WebDriver はWebDriver仕様を満たすREST APIを持つWebサーバーです。このWebサーバーは特定のブラウザを操作できる Driver を扱うことでブラウザの操作を実現します。

このREST APIを利用するクライアントが、図の左側にある WebDriver です(名前が紛らわしいですね)。

任意の言語や任意のテスティングフレームワークから WebDriver ライブラリを利用することで、E2Eテストを実施します。実際に利用する場合の全体図としては、次のようなイメージです。

f:id:Nkzn:20191125114916p:plain
WebDriverの世界観

XxxDriverService が前の図でいう Remote WebDriver にあたるはずです(違っていたらご指摘ください)。ChromeDriverのGetting StartedのJUnitでのサンプルを見ると、雰囲気を掴みやすいかもしれません。

各ブラウザを実際に操作する(おそらく複雑な)処理は XxxDriver が隠蔽してくれる上に、Remote WebDriver構成であれば、そこに指示を出す方法もWebDriver APIによって標準化されているため、テストコードからはブラウザの違いを意識することは少ないでしょう。最終的にWebDriver APIにアクセスできればよいので、多くの言語やライブラリが利用できるのも魅力的なところです。

また、WebDriverの仕組みが標準化され、Seleniumから切り離された結果として、WebdriverIOのようにNode.js上で動く(Selenium以外の)WebDriver向けテスティングフレームワークも登場しており、これが本記事のキモになっています。

通常のAppiumの使い方

WebDriverの話をしましたので、ようやくAppiumの話ができます。Appiumは、WebDriver API仕様のREST APIを提供するWebサーバーです*1

iOS向けのXCUITest DriverやAndroid向けのEspresso Driverを内部で扱うことで、モバイルアプリのネイティブUIに対して指示を出します。次の概要図でいうと、右半分にあたります。

f:id:Nkzn:20191125113603p:plain
Appiumの概要

指示は、外部のWebDriverクライアントからWebDriver APIに対してHTTPリクエストを行うことで実現します。

ただ、ブラウザ向けと完全に同じプロトコルというわけではなく、ブラウザ版がJSON Wire Protocolで通信するのに対して、Appiumサーバーと通信するためには、JSON Wire Protocolのモバイル向け拡張であるMobile JSON Wire Protocolでの通信をサポートする必要があります。そのため、ブラウザのUI操作の自動化とまったく同じライブラリが利用できるわけではありません。

Appiumサーバーにをサポートしているクライアントライブラリの一覧として、2019年11月現在、次のライブラリが紹介されています(ドキュメントから抜粋)。

Language/Framework Github Repo and Installation Instructions
Ruby https://github.com/appium/ruby_lib, https://github.com/appium/ruby_lib_core
Python https://github.com/appium/python-client
Java https://github.com/appium/java-client
JavaScript (Node.js) https://github.com/admc/wd
JavaScript (Node.js) https://github.com/webdriverio/webdriverio
JavaScript (Browser) https://github.com/projectxyzio/web2driver
Objective C https://github.com/appium/selenium-objective-c
PHP https://github.com/appium/php-client
C# (.NET) https://github.com/appium/appium-dotnet-driver
RobotFramework https://github.com/jollychang/robotframework-appiumlibrary

AppiumのOrganizationで管理されているものだけではなく、サードパーティー向けもいくつか存在していますね。

ここにもWebdriverIOが出てきました。

AppiumでWebViewをテストする

実は、AppiumにはWebViewのテストを行うための機能も搭載されています。

appium.io

Appiumの起動パラメータでもあるDesired Capabilitiesとして、autoWebview: true を設定しておくと、優先的にコンテキスト*2がWebViewに向いた状態でテストコードを実行できます。このモードについて、上記のサイトでは次のように言及されています。

Once the test is in a web view context the command set that is available is the full Selenium WebDriver API.

コンテキストがWebViewに向いている状態でテストを実施する場合、SeleniumのWebDriver APIがすべて使えるとのことです。ここでいうWebDriver APIは、ブラウザ向けの Remote WebDriver だと思っておけばよさそうです。

本記事での課題である、Cordova製iOSアプリ向けに使う場合には、次の図のような流れになるようです。

f:id:Nkzn:20191126111053p:plain
Appium経由でWebViewを操作する

Cordovaの中にあるWebViewに対して、ブラウザと同じAPIで操作ができそうな道筋が見えてきました。

WebdriverIOでAppiumを扱う

Appiumはどうにでもなりそうなのがわかってきたので、前述の図で左側にいた WebDriver の枠を埋めるツールを選びます。

今回は、WebdriverIOを利用することにしました。

webdriver.io

Appium側のドキュメントにも記載があったとおり、WebdriverIOはAppiumのクライアントとして利用できるライブラリです。WebdriverIOのサイトにも、Appiumのサポートが明記されています。

f:id:Nkzn:20191126114432p:plain
WebdriverIOはAppiumをサポート

構成のイメージとしては、次のような形になるでしょうか。

f:id:Nkzn:20191126115534p:plain
WebdriverIOでAppiumを操作する

概念図が具体的なツール名で埋まりました。それでは実際の使い方の話題に移っていきましょう。

利用の流れ

基本的な流れは公式のGetting StartedでChromeDriver Serviceを動かす方法と大きくは変わりません。

  1. NPMプロジェクトの devDependencies@wdio/cli をインストールする
  2. wdio.config.js を作成・整備する
  3. mochajasmineなどでテストコードを書く
  4. コマンドラインで $ npm test を実行してテストを実施する
    • $ npx wdio wdio.conf.js$ yarn wdio wdio.conf.js でも可

個別の設定の要点も見ていきましょう。

package.json

WebdriverIOからAppiumを操作する場合は、chromedriver@wdio/chromedriver-service の代わりに appium@wdio/appium-service を使用します。

// package.jsonの例
{
  // 略
  "scripts": {
    "test": "wdio wdio.conf.js"
  },
  "devDependencies": {
    "@wdio/appium-service": "^5.16.5", // <= 追加
    "@wdio/cli": "^5.16.6",
    "@wdio/local-runner": "^5.16.6",
    "@wdio/mocha-framework": "^5.16.5",
    "@wdio/spec-reporter": "^5.16.5",
    "@wdio/sync": "^5.16.5",
    "appium": "^1.15.1", // <= 追加
    // 略
  }
}

@wdio/appium-service はWebdriverIOがAppiumサーバーをハンドリングできるようにするためのプラグインです。次に出てくる wdio.conf.js でAppiumサーバーに関する設定をできるようになります。

wdio.conf.js

WebdriverIOの設定ファイルである wdio.conf.js の内容は、ブラウザ向けのそれとはかなり違った雰囲気になります。

まず、@wdio/appium-service のドキュメントにあるとおり、 servicesport を明示する必要があります。

// wdio.conf.jsの例
exports.config = {
  // 略
  // Appiumサーバー(WebDriver API)についての設定
  services: ['appium'], // @wdio/appium-serviceを利用してAppiumサーバーを立ち上げる
  port: 4723, // Appiumサーバーのデフォルトポート
  maxInstances: 1, // 同時に起動できるAppiumサーバーはひとつだけ
  // path: '/', // pathはデフォルトの挙動('/wd/hub')に任せる
  // 略
}

maxInstances: 1 は、Appiumが4723ポートに複数起動するのを防ぐために設定します。(参考: WebdriverIOでAppiumを使う勘所 - DeNA Testing Blog

次に、テスト対象についての設定であるDesired Capabilitiesを設定します。iOSシミュレータをiOS 13.2のiPhone 8で起動して、テストを実施したい場合の書き方です。

// wdio.conf.jsの例
exports.config = {
  // 略
  capabilities: [{
    // プラットフォーム設定
    platformName: 'iOS',
    automationName: 'XCUITest',
    // 使用するデバイスを指定する
    platformVersion: '13.2',
    deviceName: 'iPhone 8',
    // テスト対象のアプリの情報
    bundleId: "com.example.appid",
    app: 'path/to/MyCordovaApp/platforms/ios/build/emulator/HelloCordova.app',
    // テスト中の設定
    autoWebview: true, // コンテキストをWebView優先にする
  }],
  // 略
}

app は相対パスでも構わないため、WebdriverIOによるテストプロジェクトは、Cordovaプロジェクトと同居していても別々でも問題ありません。AWS Device Farm等でテストする場合は、別々のほうが都合がいいケースもあります。

autoWebview: true は既に解説したとおり、「アプリ内のWebViewを操作できるWebDriver API」が提供される、魔法の呪文です。

細かい調整は各々の手元で必要になるかとは思いますが、大枠としてはこのような設定があれば、WebdriverIOで実行したテストコードがAppiumを経由してCordovaアプリ内のWebViewに繋がります。

WebdriverIOの書き味が面白い

アプリ内のWebViewに繋がってしまえば、あとはChromeDriver等を使ってテストをする際と、ほとんど変わらない使用感になります。実際のテストは、次のような形になります。

const assert = require('assert');

// test/specs/button.js
describe('Button', () => {
  it('ボタンを押すとtextBoxが見えなくなる', () => {
    $('#hideButton').click();
    const textBox = $('#textBox');
    assert(textBox.isDisplayed() === false);
  });
});

jQueryのような記法でセレクタを記述すると、WebDriver上でのElementを表現できます。Elementから生えている click()を実行することでWebDriver APIに対してクリックコマンドを発行し、それを受けてAppiumがWebViewを操作する形です。

内部ではWebDriver APIをゴリゴリと叩く非同期処理になっているはずなので、本来はこういった処理を行う場合はasync/awaitを使うことになり、テストコードがawaitだらけになることでしょう(実際にAsync modeにするとそうなります)。

しかし、 @wdio/sync をインストールしておくことで、テストコードを同期的に記述できるようになっています。

{
  // ...
  "devDependencies": {
    // ...
    "@wdio/sync": "^5.16.5",
    // ...
  }
}

ドキュメントによれば、node-fibersを利用して、同期的な実行を実現しているそうです。

AndroidのEspressoのように、テストコードでやりたい操作を素直に記述できるため、とても好ましいと感じました。

To be continued...

手元のNode.jsで実行したテストコードで、Cordovaアプリ内のWebViewを操作する道筋ができました。この方法であれば、普通のWebアプリをテストする際とも共通項が多いため、学習コストを下げる効果が期待できます。

また、この方法であれば、AWS Device FarmのAppium Node.js環境でもテストが実行できます。後編の記事で話題にできればと思います。

後編の、Device Farmで利用する際のコツについて書いた記事を公開しました。

watercelldev.hatenablog.jp

サンプルコード

次のリポジトリでサンプルコードを公開しています。

github.com

参考文献

*1:公式ドキュメントのAppium Conceptsより

*2:Appiumが操作するターゲット