2014年12月26日

後編:Vagrant(+ VirtualBox/Ubuntu Server 14.04)環境で Appium(Node.js)と Cucumber(Ruby)を組み合わせて Android デバイス上のアプリケーションを BDD する方法

前回に続いて実機を用いた Android アプリケーションの試験自動化のお話。 後編では BDD (振る舞い駆動開発)を実現する Cucumber を使って受け入れ試験を具体的に自動化してみます。

cucumber

非 BDD なコードで Android アプリケーションを Appium で試験する


前回構築した環境の動作確認を兼ねて Appium で Android アプリケーションを操作してみましょう。

まず、仮想マシンにログイン(vagrant ssh)し appium を起動します。

vagrant@TestDroid: $ appium
info: Welcome to Appium v1.3.4 (REV c8c79a85fbd6870cd6fc3d66d038a115ebe22efe)
info: Appium REST http interface listener started on 0.0.0.0:4723
info: Console LogLevel: debug


もう一つ端末を開き note.rb という名前で次のようなコードを保存して下さい。

require 'test/unit'
require 'selenium-webdriver'
require 'appium_lib'
include Test::Unit::Assertions

deviceName = "ZenFone5"
udid = "EBAZCYXXXXXX"

apkPath = "/vagrant/NotesList.apk"
mainActivity = ".NotesList"

options = {
  appium_lib: {
    server_url: 'http://localhost:4723/wd/hub'
#    server_url: 'http://192.168.33.10:4723/wd/hub' # if you want to run this code from remote host.
  },
  caps: {
    deviceName: deviceName,
    udid: udid,
#    avd: nexus5-kk, # if you use android virtual device name.
    platformName: :android,
    app: apkPath,
    appActivity: mainActivity}
}

driver = Appium::Driver.new(options).start_driver
driver.save_screenshot("/vagrant/note_top.png")

begin
  driver.find_element(:id, "com.example.android.notepad:id/menu_add").click
  driver.save_screenshot("/vagrant/note_add.png")

  input = "Appium"
  driver.find_element(:id, "com.example.android.notepad:id/note").send_keys(input)
  driver.find_element(:id, "com.example.android.notepad:id/menu_save").click
  driver.save_screenshot("/vagrant/note_list.png")

  output = driver.find_element(:id, "android:id/text1").text
  assert_equal output, input, "ノートが一致しません(期待される結果:#{input}, デバイス上の結果:#{output})"

  puts "Success!"
rescue => ex
  puts ex.message
ensure
  driver.quit
end


試験するアプリ(NotesList.apk)は Appium で公開されている(http://appium.s3.amazonaws.com/NotesList.apk)メモ帳アプリケーション。 この apk ファイルを Vagrantfile と同じ場所に置くと仮想マシン上からは /vagrant/NotesList.apk として見えます。 仮想マシン上で直接ダウンロードする場合はこう。

vagrant@TestDroid $ wget http://appium.s3.amazonaws.com/NotesList.apk -P /vagrant/


このメモ帳アプリを動かすのが udid で指定している Andorid デバイス($ adb devices で確認出来るデバイスのID)で、複数のデバイスが1つの Appium サーバーに接続している場合にはそれらの切り替えにも利用できます。

mainActivity では自動操作するアプリケーションの起動(MAIN)アクティビティを指定しています。もし、わからない場合は apk ファイルを解凍して中に含まれる AndroidManifest.xml をデコードすれば判断できますよ。 また、自動操作するアプリケーションのエレメント(例えばボタン)は resource-id の値で指定すればOK。 この値はアプリケーションを起動させた状態で Android SDK に含まれる uiautomatorviewer コマンドを使って確認することもできるのです。

UI Automator Viewer

※ uiautomatorviewer にはデスクトップ環境が必要です。

余談ですがハイブリット・アプリケーションと呼ばれるネイティブな部品とブラウザ相当の WebView レイヤを組み合わせ殆どの機能を実装しているアプリケーションでは resource-id が何処にも現れずエ、エレメントが指定できない!と混乱することがあります。

mobile application structure


このような場合、まず driver.set_context で WebView の package (ここまでは uiautomatorviewer レベルで確認可能)を指定します。 するとドライバがブラウザ(WebView)の世界にスイッチするので Selenium で PC ブラウザを扱うかのように HTML/id 属性, xpath でエレメントを指定・操作できます。

# switch to webview
package = "net.netbuffalo.mobileapp.note"
driver.set_context "WEBVIEW_" + package

# print html code
puts driver.page_source

# switch to native app
driver.set_context "NATIVE_APP"


さあ、開発者オプション、USB デバッグを有効にした Android デバイスを PC に接続して note.rb を実行してみましょう。

vagrant@TestDroid $ ruby note.rb

指定したデバイス上でメモ帳アプリが起動し、新しいノートを作成、Appium と入力後、そのメモを保存するはずです。
(キーボードがローマ字入力以外の場合はエラーになります)

ListsNote


このテスト・コードはメモの作成・保存というシナリオに基づいて、エビデンス(画像)を残しつつ、最後に意図したノートが画面にリストされていれば合格としているわけですね。


Cucumber で実現する BDD な Android アプリケーションの試験自動化


さて先ほどのサンプル・コードは受け入れる側、言い換えると利用するユーザの視点で見てわかりやすいコードだったと言えるでしょうか? 答えはノー。 プログラムという手段が中心であり、ユーザーの目的であるアプリケーションの振る舞い検証を中心にして考えられているとは言えませんよね。

もし、同じ結果を得ることができるテスト・コードがこうだったらどうでしょうか?

# encoding: utf-8
Feature: ノート機能

Scenario: ノートの新規作成と保存
  Given I launch note application
    And I take screenshot as "note_top.png"
  When I touch 'Add note' button
    And I take screenshot as "note_add.png"
    And I fill in new note with "Appium"
    And I touch 'Save note' button
  Then I shuoud see "Appium" on the list of notes
    And I take screenshot as "note_list.png"
    And I close note application


自然言語表現に近く随分わかりやすくなりましたよね。

これは cucumber において試験仕様が定義された feature と呼ばれるファイルです(例えばこれを note.feature とします)。 Given は前提条件、When は試験の目的を表す振る舞い、Then は期待される結果です(And はそれぞれに付帯する行為。 詳しくはこちら => https://github.com/cucumber/cucumber/wiki/Given-When-Then)。

しかし、こんなシンタックスのプログラミング言語があるわけない・・・。 その実体はどこにあるんでしょうか? これは steps と呼ばれるファイルに記述します。 つまり、ユーザの目的(要件に基づいた振る舞いの確認)とそれを実現するテスト・コードという手段が分離されたのです!

この手段を詰め込んだ steps ファイルを note.steps ファイルと呼ぶならば内容はこうなります。 条件にマッチした文節に応じてコードが実行されるんですね(実際のところ文節を評価するのに Given、When、Then、And の何れであるかは関係無く、同じコードを2度書く必要はありません)。

Given(/^I launch note application$/) do
  if @driver.nil?
    @driver = Appium::Driver.new(@options).start_driver
    #@driver.launch_app
  end
end

When(/^I touch 'Add note' button$/) do
  @driver.find_element(:id, "com.example.android.notepad:id/menu_add").click
end

When(/^I touch 'Save note' button$/) do
  @driver.find_element(:id, "com.example.android.notepad:id/menu_save").click
end

When(/^I fill in new note with "(.*?)"$/) do |input|
  @driver.find_element(:id, "com.example.android.notepad:id/note").send_keys(input)
end

Then(/^I shuoud see "(.*?)" on the list of notes$/) do |input|
  title = @driver.find_element(:id, "android:id/text1").text
  assert_equal title, input, "ノートが一致しません(期待される結果:#{input}, デバイス上の結果:#{title})"
end

Then(/^I close note application$/) do
  @driver.close_app
  @driver.quit
end

And(/^I take screenshot as "(.*?)"$/) do |capfile|
  @driver.save_screenshot(@screenshot_dir + "/" + @deviceName + "_" + capfile)
end


技術が大好きなエンジニア(但し、ユーザ視点で話すのはちょっと苦手でテクニカルな方向に脱線しやすい・・・)には、この steps の編集に集中して貰うことができます。

最後に env.rb というファイルを紹介しましょう。 これは設定ファイルと考えればよし。 Before、After ブロック(フック)は Scenario が評価される前・後に実行されるコードです(AfterStep は step ファイルのステップ毎、at_exit は Ctrl+C などで中断した際に実行されるブロック。 フックの詳細はこちら => https://github.com/cucumber/cucumber/wiki/Hooks)。

このブロック内で定義したインスタンス変数(@)はステップ内のコードとオブジェクトを共有できます。 feature の Scenario がクラス、各ステップがメソッド、そして env.rb の Before がコンストラクタと考えればスッキリします。

require "selenium-webdriver"
require 'appium_lib'

server_url = 'http://localhost:4723/wd/hub'
deviceName = "ZenFone5"
udid = "EBAZCYXXXXXX"
apkPath = "/vagrant/NotesList.apk"
mainActivity = ".NotesList"
screenshot_dir = "/vagrant"

options = {
  appium_lib: {
    server_url: server_url
   },
  caps: {
    deviceName: deviceName,
    uuid: udid,
    platformName: :android,
    app: apkPath,
    appActivity: mainActivity}
}

# singleton
# driver = Appium::Driver.new(options).start_driver

Before do |scenario|
  @screenshot_dir = screenshot_dir
  @deviceName = deviceName
  @options = options
end

AfterStep do |scenario|
  # AfterStep hooks will be run after each step (Given, When, Then, And,...).
  sleep 0.1
end

After do |scenario|
  # After hooks will be run after the last step of each scenario.
  if scenario.failed?
    if @driver
      @driver.quit # close session
    end
  end
end

at_exit do
  if @driver
    @driver.quit  # close session
  end
end


After、Before ブロックは Scenario 毎に実行、ブロック外に記載されているコードは1度だけ評価されることにも注意して下さい。 例えば driver の初期化をテスト中に一度だけ実行し、その後はインスタンスを再利用したい(※1)と考えたならばブロック外で初期化(new)してブロック内からはそのオブジェクトをインスタンス変数で参照するということになります。

※1 Before ブロック内で Appium::Driver.new(options).start_driver すると都度 apk ファイルをデバイスに push、インストールを行うので時間は掛かります。但し、各シナリオの開始前に必ずアプリケーションが初期化されクリーンな状態でテストを開始できるというメリットもあります。


これらの3つのファイルをこんな構成で配置して、

notebook
└── features
    ├── note.feature
    ├── step_definitions
    │ └── note_steps.rb
    └── support
        └── env.rb


cucumber コマンドで feature ファイルを指定(無指定の場合 features 以下の全ての *.feature が対象)するとテストが実行されますよ。

vagrant@TestDroid $ cd notebook
vagrant@TestDroid:~/notebook $ cucumber features/note.feature

# encoding: utf-8
Feature: ノート機能

  Scenario: ノートの新規作成と保存                             # features/note.feature:4
    Given I launch note application                 # features/step_definitions/note_steps.rb:1
    And I take screenshot as "note_top.png"         # features/step_definitions/note_steps.rb:30
    When I touch 'Add note' button                  # features/step_definitions/note_steps.rb:8
    And I take screenshot as "note_add.png"         # features/step_definitions/note_steps.rb:30
    And I fill in new note with "Appium"            # features/step_definitions/note_steps.rb:16
    And I touch 'Save note' button                  # features/step_definitions/note_steps.rb:12
    Then I shuoud see "Appium" on the list of notes # features/step_definitions/note_steps.rb:20
    And I take screenshot as "note_list.png"        # features/step_definitions/note_steps.rb:30
    And I close note application                    # features/step_definitions/note_steps.rb:25

1 scenario (1 passed)
9 steps (9 passed)
0m30.279s


1つの feature ファイルに複数のシナリオを定義するのもOK。 steps ファイルは各 feature で共通のステップと特定の feature だけに特化したものを分け、別ファイルで管理するのが良さそうです。

ふう、ちょっと調べるつもりが長い話になっちゃいましたね。

アジャイルサムライ−達人開発者への道−
Jonathan Rasmusson
オーム社
売り上げランキング: 3,929

実践 Selenium WebDriver
実践 Selenium WebDriver
posted with amazlet at 14.12.26
Satya Avasarala
オライリージャパン
売り上げランキング: 8,162

Posted by netbuffalo at 21:15│Comments(0)TrackBack(0)プログラミング | Android


この記事へのトラックバックURL

http://trackback.blogsys.jp/livedoor/netbuffalo/4936081

コメントする

名前
URL
 
  絵文字