21
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Fringe81Advent Calendar 2019

Day 21

アプリテスト自動化への旅【AppiumとJestで簡単に試してみよう!】

Last updated at Posted at 2019-12-20

皆さんはアプリのテスト自動化をしたことがありますか?
おそらく手元で手動テストをし、Excelか何かにまとめられたテスト仕様書にチェックを入れている人もいるのではないでしょうか。

しかし、自動化しようとしても、**どこまで自動化ができるのだろうか。。。**だとか、**こんな表現をテストで実装は出来ないよね?**だとか、**時間かかりそうだし、今はそういうフェーズじゃない。**だとか...そう思っている人は結構いるのではないかと私は思っています。

そんな人たちに、意外とアプリのテスト自動化はイケるぞ!ってのを伝えるために、本記事にて、AppiumJestを組み合わせた自動テストを紹介します。

皆さんの手元で簡単にセットアップできるので、是非お試しください!

なお、今回紹介する内容のソースコードは以下のリポジトリから参照できますので、是非ご利用ください!
https://github.com/minakawa-daiki/AppiumJestSample

目標

今回の記事では、以下が出来ることをゴールにしていきたいと思います。

  • ページがしっかりと表示されている
  • テキストを自動入力できる
  • ボタンが押せる
  • スワイプができる
  • ブラウザアプリを開いて、テスト対象のアプリに戻ってくる
  • スクリーンショットを保存してみる
  • 一連のテストフローを動画として保存する

Appiumについて

公式サイト: https://appium.io/

公式サイトの言葉を借りて、Google翻訳すると

Appiumは、ネイティブ、 ハイブリッド、およびモバイルWebアプリで利用可能なオープンソーステスト自動化フレームワークです。
WebDriverプロトコルを使用してiOS、Android、およびWindowsアプリで動きます。

要するにアプリのテストを自動化できます。使う前は、どこまで自動化できるのだろうか?と疑問でしたが、かなり自動化できる印象です。様々な言語に対応しているので、とても使いやすいです。

また、GUIのデバッグツールなども用意されているので、とても開発者に優しい、素晴らしいプロダクトだと思います。

Appium

画像は https://www.3pillarglobal.com/insights/appium-a-cross-browser-mobile-automation-tool より

Appiumは様々な言語に対応していますが、今回はJest + TypeScriptで記述していきます。
また、ライブラリはwdを使って書かれたサンプルが多いですが、今回はwebdriverioでやっていきます。

下準備

AndroidとiOSの両方のテスト自動化を行なっていきたいため、 React Nativeでサンプルアプリを作成します。
今回使用するバージョンは以下の通りになります。

  • Expo SDK: v36
  • Appium: 1.16.0-beta.3

Appiumは執筆当時、iOS10系周りでバグが出ていたため、betaを使用しています。
https://github.com/appium/appium/issues/13627

iOS Simulatorの準備

テストに使用するiOS Simulatorの準備をまずしましょう。

Xcodeを開いて、使用したいOSバージョンのシミュレーターをまずはインストールします。

image.png

インストールした後、Simulatorの一覧に存在する端末であれば、その端末を利用してテストが実行されます。
存在しない端末だと毎回新しいSimulatorが作られてしまうので、テストが落ちる原因になるので少し注意が必要です。

image.png

今回の例だと、iPhone11 Pro MaxのiOS 13.2という感じです。

Android Emulatorの準備

AndroidのEmulatorも以下の手順に従って準備していきます。

  1. Android Studioで適当なサンプルアプリを開いてAVDマネージャを開きます
    image.png
  2. Create Virtual Deviceからエミュレータを追加します
  3. Nameの部分を利用するのでメモっておくと良いでしょう

ここで注意点なのですが、Android 6系以下のEmulatorはChromeではないため(実機はChrome)、WebViewを使用しているアプリでは、そもそもテスト自動化の検証端末に使用しない方がいいです。実機を利用しましょう。

参考: https://qiita.com/masakura/items/210261c954256a7879e6

Appium doctorの実行と修正

projectフォルダに移動し、 yarn appium-doctor を実行しましょう。そして、エラーが出た部分を直していきます。

Macユーザーの方は以下の項目でエラーが出る確率が高いと思います。

  • ✖ Xcode is NOT installed!
  • ✖ Carthage was NOT found!
  • ✖ ANDROID_HOME is NOT set!
  • ✖ JAVA_HOME is NOT set!
  • ✖ adb could not be found because ANDROID_HOME is NOT set!
  • ✖ android could not be found because ANDROID_HOME is NOT set!
  • ✖ emulator could not be found because ANDROID_HOME is NOT set!
  • ✖ Bin directory for $JAVA_HOME is not set

まず、 Xcode is NOT installed!https://github.com/nodejs/node-gyp/issues/569#issuecomment-94917337 を参考に、Xcodeをインストールした後、 udo xcode-select -s /Applications/Xcode.app/Contents/Developer を実行しましょう。

Carthage was NOT found!brew install carthage してください。

ANDROID_HOME is NOT set!JAVA_HOME is NOT set! は以下のような感じで環境変数を設定しましょう。

export ANDROID_HOME=$HOME/Library/Android/sdk
export JAVA_HOME=`/usr/libexec/java_home`
export PATH=$JAVA_HOME/bin:$PATH

そして、もう一度 yarn appium-doctor をすると解消しているはずです。

そして今回は、テストの実行状況を録画するため、ffmpegをインストールしておきます

brew install ffmpeg

今回のサンプルアプリの概要

今回使用するサンプルアプリは以下のようなページを持っています。コードの内部実装は詳しく説明しませんが、ソースコードは公開しているので、気になる方はご覧ください。

  • 1ページ目はテキストを入力する画面があります
  • 2ページ目は1ページ目で入力されたテキストが表示されます。ボタンを押すことで内容が変わります
  • 3ページ目は画像が配置されたWebViewで、画像をクリックするとSafariに遷移します
  • 4ページ目は空のページです
  • 5ページ目は動画が配置されたWebViewで、動画をクリックするとSafariに遷移します

hoge.gif

アプリを取得する

GitHubにてapkとappを公開してますが、自分でビルドしたい場合はexpo経由で以下のコマンドを実行してください。

自分でビルドする場合

  • iOSの場合は yarn build:ios
  • Androidの場合は yarn build:android

直接ダウンロードする場合

また、生成されたアプリはappフォルダ直下にAppiumJestSample.appAppiumJestSample.apkでそれぞれ配置してください。(AppiumJestSample.app.zipを解凍してご利用ください)

テスト自動化対象端末のconfigを設定する

今回はテストコードを書く上のドライバーとしてwebdriverioを採用していますので、jest-environment-webdriverioというライブラリを使用しています。
https://www.npmjs.com/package/webdriverio
https://www.npmjs.com/package/jest-environment-webdriverio

また、よくAppiumのテストで使用されているドライバーでは wd があります。
https://www.npmjs.com/package/wd

webdriverioを採用した理由は、非同期処理をより綺麗に書ける点もありますが、執筆時点のwdでは動画撮影周りにバグがあったため採用を見送りました。

jest-environment-webdriverioのおかげで、webdriverioの設定を簡単に記述できます。jest.config.js を実際に見てみましょう。

必要な設定はtestEnvironmentjest-environment-webdriverioを指定し、testEnvironmentOptionsに端末情報を記述するだけです。

iOSでは以下のような設定になります。

jest.config.js
{
  port: 4723,
  capabilities: {
    platformName: "iOS",
    platformVersion: "11.3",
    deviceName: "iPhone X",
    automationName: "XCUITest",
    wdaLocalPort: 8100,
    nativeWebTap: true,
    app: "./app/AppiumJestSample.app"
  }
}
  • portはAppiumが起動しているポートを指定します
  • platformVersionはiOSのバージョンです
  • deviceNameはiOS Simulatorに存在する端末を指定します
  • automationNameは実際に使用するUIテストフレームワークを指定します
  • wdaLocalPortは並列化するときに違うポート番号を指定します
  • appはiOSのアプリケーションのpathを指定します

Androidはこんな感じです。

jest.config.js
{
  port: 4723,
  capabilities: {
    platformName: "Android",
    deviceName: "Android Emulator",
    automationName: "Appium",
    avd: "Pixel_3_API_29",
    systemPort: 8200,
    nativeWebTap: true,
    app: "./app/AppiumJestSample.apk"
  }
}
  • deviceNameはエミュレータを使用する場合はAndroid Emulatorを指定します
  • avdはエミュレータのnameのスペースを_で埋めたものを記述します
  • systemPortは並列化するときに違うポート番号を指定します

また、実機を接続してる場合は、以下のような設定で動かせます。

jest.config.js
{
  port: 4723,
  capabilities: {
    platformName: "Android",
    systemPort: 8201,
    app: "./app/AppiumJestSample.apk"
  }
}

自動テストを動かす

初回は自動化するためのアプリをインストールするため、時間がかかります。テストが失敗することが多々あるので、その場合は再度実行しましょう。

テストを実行する前にバックグラウンドで yarn appium を実行し、Appiumサーバーを立ち上げておきましょう。

その上で、テストの実行はyarn testで動作します。デフォルトはiOSが起動します。
明示的にOSを指定したい場合はそれぞれyarn test:android, yarn test:iosを実行してください。

テストが開始されると、シミュレータやエミュレータが起動し、テストが行われます。
全てのテストが終わったら、tests/screenshotsにテスト途中のスクショや全体の動画が以下のような感じで保存されていると思います。

image.png

hoge.gif

Appiumのスタイルはjestなどのテストフレームワークで表示チェックを担保するのと、スクリーンショットや動画なのでアプリの全体的な動きのテストを担保することで自動化していくのが良いと思っています。

無理に全てテストコードで担保しようとせず、スクリーンショットや動画も積極的に使っていきましょう。

テストコードの説明

まず、ReactNativeとNativeで書かれたコードのテストコードは結構違う感じになることを念頭に置いておいてください。
また、今回はほんの一部しか機能を紹介しないので興味を持った方はドキュメントを見てみると良いでしょう。

ドキュメントの例: http://appium.io/docs/en/commands/element/find-element/

それに加え、テストコードを書くときにはアプリ内のそれぞれのElementに対してaccessibilityLabelを振っておくと要素を特定しやすくなるのでオススメです。

動画を撮影する

index.test.ts
beforeAll(async () => {
  try {
    await browser.startRecordingScreen({ videoType: 'mpeg4' });
  } catch (e) {}
});

afterAll(async () => {
  try {
    const movie = await browser.stopRecordingScreen();
    const decode = Buffer.from(movie, 'base64');
    fs.writeFileSync(
      `${baseResultPath}/result.mp4`,
      decode
    );
  } catch (e) {}
});

動画は簡単に撮れます。browser.startRecordingScreen({ videoType: 'mpeg4' })で撮影を開始した後、browser.stopRecordingScreen()で結果を保持し、後はデコードして書き出すだけです。

ffmpegをインストールしておくのを忘れずに。

「1ページ目が存在している」かどうかのテスト

index.test.ts
test('1ページ目が存在している', async () => {
  expect((await browser.$('~slide1')).elementId).not.toBeUndefined();
  await browser.saveScreenshot(`${baseResultPath}/page1.png`);
});

browser.$('~slide1')).elementIdundefinedとならない場合、そのページは確かに存在している。という風にしてテストを担保しています。これは各ページで行っています。

また、browser.saveScreenshot('${baseResultPath}/page1.png')でスクリーンショットを保存しています。簡単ですね!

「1ページ目で入力された内容が2ページ目で表示されている」かどうかのテスト

index.test.ts
test('1ページ目で入力された内容が2ページ目で表示されている', async () => {
  const textInputElement = await browser.$('~TextInput');
  await textInputElement.addValue('おりばー');
  await browser.pause(2000);
  await browser.hideKeyboard();
  await swipe('left');
  // 2ページ目の存在確認
  expect((await browser.$('~slide2')).elementId).not.toBeUndefined();

  // https://github.com/appium/appium/issues/13288 をみる限り
  // iOSのバージョンによってはgetText()がaccessibilityLabelと同じ文字列を返すバグがある
  // なのでaccessibilityLabelの値をinputTextと一緒にしている
  // 最新のiOSとAndroidは問題ない
  const textInputResultElement = await browser.$(`~${inputText}`);
  expect(await textInputResultElement.getText()).toBe(inputText);
});

async function swipe(direction: 'left' | 'right') {
  if(platformName === 'iOS') {
    // iOSはswipeを呼び出せるのでそれを使う
    await browser.execute('mobile: swipe', { direction });
  } else if(platformName === 'Android') {
    // Androidは代わりにflickを利用する
    const rootElement = await browser.$('//*');
    const x = direction === 'left' ? -1000 : 1000;
    await browser.touchFlick(x, 0, rootElement.elementId, 100);

    // 現状ReactNativeはこれだとSwipeできない?(AndroidのNativeだと動くことを確認)
    // await browser.touchPerform([
    //   { action: 'press', options: { x: 1000, y: windowSize.height / 2 } },
    //   { action: 'moveTo', options: { x: 100, y: windowSize.height / 2 } },
    //   { action: 'release' },
    // ]);
  }
}

ここでは、1ページにて、文字を入力し、それが2ページ目で入力された内容が表示されているかどうかのテストを行っています。

textInputElement.addValue('おりばー')とすることで、textInputElementに対して「おりばー」という文字が入力されます。

また、browser.hideKeyboard()でキーボードを閉じることができるのも意外と盲点だったりします。

swipe('left')はスワイプを行なっています。iOSとAndroidでスワイプの仕方が様々あるので、気になる方はコードを読んでみてください。

browser.$(~${inputText})ここの部分はコメントにも書いているのですが、iOSの特定のバージョンにおいて、getText()accessibilityLabelで指定した内容になってしまうというバグが存在しています。

追記: Xcodeのバージョンも大きく関係しているらしいです。詳しくはコメント欄をご覧ください。

ですので、その応急処置として、accessibilityLabelにそのまま値を入れるという実装になってしまっています。

「2ページ目のボタンをタップすると内容が変化する」かどうかのテスト

index.test.ts
test('2ページ目のボタンをタップすると内容が変化する', async () => {
  await browser.saveScreenshot(`${baseResultPath}/page2-1.png`);
  const textChangeButtonElement = await browser.$('~textChangeButton');
  await textChangeButtonElement.click();
  const textInputResultElement = await browser.$('~タップされたよ!');
  await browser.saveScreenshot(`${baseResultPath}/page2-2.png`);
  expect(await textInputResultElement.getText()).not.toBe(inputText);
});

ここではtextChangeButtonElement.click()でボタンをタップして、文字の内容が変わっていることをテストしています。

「3ページ目が存在している」かどうかのテスト

index.test.ts
test('3ページ目が存在している', async () => {
  await swipe('left');
  expect((await browser.$('~slide3')).elementId).not.toBeUndefined();
  await browser.saveScreenshot(`${baseResultPath}/page3.png`);

  // 画像をクリックして戻ってこれることを確認
  await browser.pause(2000);
  const imageWrap = await browser.$("~imageWrap");
  await imageWrap.click();
  await browser.pause(5000);
  // アプリに戻ってくるための処理
  if (platformName === 'Android') {
    await browser.pressKeyCode(4);
  } else {
    await browser.execute('mobile: activateApp', {
      bundleId: app.expo.ios.bundleIdentifier,
    });
  }
});

3ページ目では存在確認に加え、「画像をクリックして、別アプリに行った後、戻ってこれることを確認」しています。
別アプリに遷移してしまった後はAndroidとiOSとで指定の仕方は変わるのですが、それぞれ以下のコマンドで可能になっています。

Androidの場合
browser.pressKeyCode(4)

iOSの場合
browser.execute('mobile: activateApp', {bundleId:app.expo.ios.bundleIdentifier,})

簡単ですね!

「5ページ目が存在している」かどうかのテスト

index.test.ts
test('5ページ目が存在している', async () => {
  await swipe('left');
  expect((await browser.$('~slide5')).elementId).not.toBeUndefined();

  // 動画が再生されていることをスクショで判別する
  await browser.saveScreenshot(`${baseResultPath}/page5-1.png`);
  await browser.pause(3000);
  await browser.saveScreenshot(`${baseResultPath}/page5-2.png`);
});

5ページ目では動画が自動再生されますので、スクリーンショットを撮って、しっかりと再生されているかをチェックしています。

こんな感じでスクリーンショットを撮りつつ、テストを実行していく流れになります。

余談

今回のテストの中で、Androidの場合はChromeDriverを利用するのですが、ChromeDriverはChromeのバージョンと強く紐づいています。
ですので、本来であればAndroidのChromeバージョンによって使い分けなければいけないのですが、Appiumの起動時にchromedriver_autodownloadと指定することで、**Appium側でそこをよしなにやってくれています。**すごいですね!

並列化について

並列化はwebdriverioが提供しているものもありますが、それを利用して並列化すると特定の端末のテストが落ちたときに全てのテストが失敗してしまうことになるのでオススメはあまりしないです。

なので、自分で別プロセスでそれぞれ実行して並列化させることをオススメします。

並列化するときの注意点はiOSの場合はwdaLocalPortを、Androidの場合はsystemPortをずらして実行することです。

終わりに

アプリのテスト自動化はAppiumのおかげで想像よりかは書きやすいです。
しかし、バグも多く潜んでいるので、根気よく戦っていく形にはやっぱりなってしまいます。
無理して全てを自動化するのではなく、今手動でやっているテストを少しでも簡単にしようという目線で、少しずつ自動化していくことが良いのではないかと思います。是非皆さんもAppiumに触れてみてください。

21
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?