最近、いくつかCI環境のサービスを調べているのですが、どうせならモバイルアプリのテストも外サービスに出して、サービス開発に力を注げるような環境を構築できたらなと思いSauceLabsを使ってみました。
SauceLabsを使った理由としては、Appiumの開発に強く関わっているところだからなだけです。
良い点
- 各々の操作時にスクリーンショットをSauceLab側でキャプチャしてくれるので、シナリオの中でどこでキャプチャをとるとかをいちいち考えなくてよい
- この操作起因でAppium Serverが不安定になる懸念を除ける!
- ビデオも撮ってくれる
- 不具合発生時の再現画面をみることができる
- 実行環境のメンテナンスを外に任せることができる
- iOSもAndroidも同時実行できる環境を用意するのは結構面倒。
- Appium + Seleniium Gridや、少し頑張れば自前でも分散環境作れるけれど
- 自前で少し作ったことありますが、保守する所を外に出せるって、テストに集中できるのであり互いのですよね。
- AppiumをOSSとして提供しているので、シナリオ自体の動作確認を手元でできる(料金体系の外でできる)のは良い
懸念点
- Pricingの体系が、月々の価格によっている
- E2Eのテストは多くの場合実行に時間がかかるので、優先度をつけての良い取捨選択を結構シビアにする必要がでてきそう
- DailyやWeeklyというような間隔で実行する形でないと、例えばコミット毎とかは時間の縛り上無理そう
では、内容。
Androidに関しては、実際に試用したコードも載せてます。
SauceLabsを使ってみた
試してみたのは、このチュートリアル
テストの実行
以下のような形でテストシナリオを実行します。実行すると、SauceLabにセッション接続し、シナリオなどをあちらに渡します。
SauceTest.rbがテストシナリオを記載しているもので、RSpecによる例がチュートリアルでは使われています。
$ SAUCE_USERNAME=<username> SAUCE_ACCESS_KEY=<key> rspec SauceTest.rb ..Finished in 53.13 seconds
iOSもAndroidも私が実行したときはともにチュートリアルそのままではFailになり成功しませんでした。。。
iOSに関しては、原因はわかるのでその原因が発生しない形に書き直して再実行したのですが成功せず。手元でAppiumたてて実行したときは成功するのですが、うーん。
Androidに関してはiOSのものをベースに、シナリオをAndroidのものに置き換えて少し修正すればうまくいきました。must_equalがチュートリアルでは使われていたのですがundefined methodと言われたので、expectで書き換えてます。成功した形のコードはページの下側に貼付けています。
Session毎のテストケースの実行結果画面
実行結果みるときは、ログイン後にホームに訪れると以下のような表示を確認することができます。

テストケースの内約
その内約は以下のように確認でき、実行時の動画をScreencastというページで見ることができます。
他、AppiumのLogも見ることができるので、何か問題あったりするときも解析に困らなさそう。

iOSとして指定可能なもの
スクリプトによる自動テストの他、シロッコクラウドのような形でマニュアルリモートできる環境もあります。
iOSは以下を指定できます。

Androidとして指定可能なもの
Androidは以下。マニュアルではこれだけなのですが、自動実行時は、特定の端末指定でいくつかの端末はテストできます。

サイドバー
こんな感じで利用可能な残り時間などが表示されています。

最後に
使ってみた感想としては、結構よさげ。動画撮影とかは、サービス利用者にはSauceLab側が責任持ってくれるところが良い。Appiumを提供している企業なだけに、Appiumで作っている資産(他言語による共通のシナリオ作成可能な環境など)は十分機能していました。自動テストに比較的容易に落とし込めるテスト対象は人手をかけたくないので、こういうシナリオを実行できる環境よいな。
Tavis CIなどのCI環境
=> SauceLabにテストスクリプトを投げて、成果物に対してスモークテストを実施(5~10分以内で終わるレベル)
=> 成果物をDeploygateなどで配信
=> 探索テストや、リリース前には多機種試験などを実施
=> リリース
という流れができれば、かなり近代的な感じがする。
余談
最近、やっぱりシナリオはTurnipとRSpecのハイブリッドが良いのかなと思うようになってきた。Turnipの利点は、コード読まずに、人の操作ベースで何しているかをさっと理解できるところ。
コードの意味を理解したいとか、そういう領域ではなく、ここのレベルで求められるべきはおそらくどういう操作ができてほしいか、という話しなので、そういう意味では、コードは地を這うような表現に寄り過ぎていると思うのですよね。
では。
- 以下は、チュートリアルで出てくるSauceTest.rbを、Android向けにカスタマイズしたコードです。何回かトライして成功した・・・
require 'rubygems'
require 'spec'
require 'appium_lib'
require 'sauce_whisk'
SAUCE_USERNAME = ENV['SAUCE_USERNAME']
SAUCE_ACCESS_KEY = ENV['SAUCE_ACCESS_KEY']
# This is the test itself
describe 'Computation' do
before(:each) do
Appium::Driver.new(desired_caps).start_driver
Appium.promote_appium_methods RSpec::Core::ExampleGroup
end
after(:each) do
# Get the success by checking for assertion exceptions,
# and log them against the job, which is exposed by the session_id
job_id = driver.send(:bridge).session_id
update_job_success(job_id, example.exception.nil?)
driver_quit
end
it 'can create and save new notes' do
find('New note').click
first_textfield.type 'This is a new note, from Ruby'
find('Save').click
note_count = ids('android:id/text1').length
expect(note_count).to eq 1
expect(texts.last.text).to eq 'This is a new note, from Ruby'
end
end
def desired_caps
{
caps: {
:'appium-version' => '1.2.0',
platformName: 'Android',
platformVersion: '4.3',
deviceName: 'Android Emulator',
app: 'http://appium.s3.amazonaws.com/NotesList.apk',
name: 'Ruby Appium Android example'
},
appium_lib: {
wait: 60
}
}
end
def auth_details
un = SAUCE_USERNAME
pw = SAUCE_ACCESS_KEY
unless un && pw
STDERR.puts <<-EOF
Your SAUCE_USERNAME or SAUCE_ACCESS_KEY environment variables
are empty or missing.
You need to set these values to your Sauce Labs username and access
key, respectively.
If you don't have a Sauce Labs account, you can get one for free at
http://www.saucelabs.com/signup
EOF
exit
end
return "#{un}:#{pw}"
end
def server_url
"http://#{auth_details}@ondemand.saucelabs.com:80/wd/hub"
end
def rest_jobs_url
"https://#{auth_details}@saucelabs.com/rest/v1/#{SAUCE_USERNAME}/jobs"
end
# Because WebDriver doesn't have the concept of test failure, use the Sauce
# Labs REST API to record job success or failure
def update_job_success(job_id, success)
RestClient.put "#{rest_jobs_url}/#{job_id}", { 'passed' => success }.to_json, :content_type => :json
end