おがさわらなるひこのオープンソースとかプログラミングとか印刷技術とか

おがさわらなるひこ @naru0ga が技術系で興味を持ったりなんだりしたことをたまーに書くブログです。最近はてなダイアリー放置しすぎて記事書くたびにはてな記法忘れるのではてなブログに移行しました。

クリエイティブ・コモンズ・ライセンス
特に断りがない場合は、本ブログの筆者によるコンテンツは クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスの下に提供されています。

docker-android + AppImage版Appium DesktopでAndroidテスト自動化の環境をさっくり作る

うっすいネタ、いわゆる「使ってみました」ネタで恐縮です。あと、タグSeleniumってつけてますが兄弟?プロジェクトのAppiumネタです。

私の本業はいちおうソフトウェア自動テストアーキテクト*1 なわけです。主にSeleniumを使ったWebの自動テストが得意領域ってことになってるんですが、その周辺領域であるAppiumを用いたモバイルの自動テストはあんまり手を動かした経験がなく、教科書レベルのことは言えるけど……ってのがコンプレックスでした。

最近いろいろあって残業することなく早く帰るようにしてるので、自由になる時間をYouTubeの動画*2 観て溶かしてないで、ちったー手を動かしてみますかーってことで、マイパソコンでAppiumのテスト自動化開発環境を作ろうと思い立ちました。仕事ではJava + Selenide*3 なんですけど、まあ個人の遊びなので違う言語ってことでRubyで。

ご存知の通り?私は個人ではUbuntuしか使ってないので、Ubuntu上で環境を作りたい。なのですが、AppiumってNodeアプリなので、Nodeを入れなきゃなのですが、NodeもまたこれがLinuxディストリビューションのパッケージングポリシーと合わないことおびただしい*4 のでコンテナに閉じ込めたりしたい。さらにいうとAndroid SDKとかの管理もめんどくさい。なんかいいのないかなーとググっていたら。素晴らしいものがありましたよ。

GitHub - butomo1989/docker-android: Android in docker solution with noVNC supported and video recording

AndroidのAVDとAppiumを封じ込めたコンテナ。docker run一発で全部入り環境が立ち上がる。AVDの操作ははVNCとか入れずにブラウザーで確認可能。動画記録機能まである。これは最高なのでは……。

ということで使ってみました。

Ubuntuのバージョンは18.04 LTS(bionic beaver)です。

docker-android使ってみよう

むっちゃ簡単です。

docker run --privileged -d -p 6080:6080 -p 5554:5554 -p 5555:5555 -p 4723:4723 -e DEVICE="Samsung Galaxy S6" -e APPIUM=true --name android-container butomo1989/docker-android-x86-8.1

-e APPIUM=true でAppiumサーバーも一緒にあげてます。

ちょっとコンテナサイズでかくて、初回実行時にはpullにしばらく待たされました*5 けど、あっさり動いた。ちょっと感動。

f:id:naruoga:20180713222744p:plain

AppImage版Appium Desktopからつなぐ

Appiumサーバーちゃんと動いてること確認したいですね。

そのためには(もちろん、今後使うからでもありますけど)Appiumの開発ツールであるAppium Desktopも使いたい。しかしこいつも依存関係汚すのはやだなー。そう思いつつ公式の配布先↓ 見に行ったら。

github.com

あるじゃん。AppImage版。

AppImageについては深入りしませんが*6、よーはファイル取ってきて実行権限つけるだけで、インストール不要で使えるパッケージ。これなら環境汚さないしもう使わないと思ったらファイル消すだけでOKです。

早速取ってきて(依存関係全部抱いてる関係でちょっとでっかいです)、実行権限つけて起動。

f:id:naruoga:20180713223522p:plain

今回はdocker-android内でAppiumサーバー動いてるので「Start Server」は押さずに、メニューから「File」>「New Session Window」を起動。セッション画面を起動します。

で、Desired Capabilitesを以下のようにして*7、単に設定画面を開いてみます。

{
  "platformName": "android",
  "deviceName": "device",
  "appActivity": "Settings",
  "appPackage": "com.android.settings",
  "takesScreenshot": false
}

Start Session! うりゃ!

f:id:naruoga:20180713224510p:plain

ちゃんと動いてる! やったね。

スクリプトから起動してみる

ではRubyスクリプトから起動してみましょうか。せっかくなので今度はアプリケーションをサイドロードして起動して、それを操作してみたいですね。

っと、その前に、apkを導入するときには、apkのあるフォルダを /root/tmp にマウントしろとドキュメントに書いてあるので、動いてるコンテナを止めて再起動します。スクリプトを作るフォルダにapkは置くことにして、雑に $PWD をマウントします。

docker stop android-container
docker rm android-container
docker run --privileged -d -p 6080:6080 -p 4723:4723 -p 5554:5554 -p 5555:5555 -e DEVICE="Samsung Galaxy S6" -e APPIUM=true -e CONNECT_TO_GRID=true -e APPIUM_HOST="127.0.0.1" -e APPIUM_PORT=4723 -e SELENIUM_HOST="172.17.0.1" -e SELENIUM_PORT=4444 -e MOBILE_WEB_TEST=true -v $PWD:/root/tmp  --name android-container butomo1989/docker-android-x86-8.1

あれ、コマンド履歴見直すと CONNECT_TO_GRID*8 はいらんのでは……。まあいいや。

アプリは、わたくし普段からAndroidアプリ開発してるわけではないので*9 手持ちのapkはなく、ので、Appiumのサンプル からApiDemos-debug.apkを取ってきて手元のディレクトリに置きました。

で、次のようなRubyスクリプトを書きます。

require 'test/unit'
require 'appium_lib'

class SimpleTest < Test::Unit::TestCase
    def setup                 
        desired_caps = {
            caps: {
                platformName: 'android',
                deviceName: 'device',
                app: '/root/tmp/ApiDemos-debug.apk',
                appActivity: '.ApiDemos',
                appPackage: 'io.appium.android.apis', 
                takesScreenshot: false,
            },
            appium_lib: {
                server_url: 'http://localhost:4723/wd/hub'
            }
        }

        driver = Appium::Driver.new(desired_caps, true)
        Appium.promote_appium_methods self.class
        driver.start_driver.manage.timeouts.implicit_wait = 20
    end
    
    def teardown
        driver_quit
    end

    def test_sample
        puts "hello"
    end
end

これでさくっと動いてしまいました。いやーすばらしい。

f:id:naruoga:20180713232604p:plain

今のところテスト本体は空っぽなので、単にアプリ起動できたところまでしか確認できてませんけど。

これから、テストちょろちょろ書いていこうと思います。今回は 'test/unit' 使ってるけど、このテストフレームワークがいいよ! という推薦があれば教えていただきたいです。かなり昔に教えてもらったTurnipが気になってますが……。

まあともかく、docker-androidおすすめです。あとLinux使うならAppImage版Appium Desktopもチョー推薦です*10

以下おまけあり。読みたい方は「続きを読む」をクリックしてください。


実はこんなにあっさりうまく行きませんでした。わたくしが間抜けなのがいけないのですけど。

失敗編

最初書いたスクリプトはこんな感じでした。違いは後ほど。

require 'test/unit'
require 'appium_lib'

class SimpleTest < Test::Unit::TestCase
    def setup                 
        desired_caps = {
            caps: {
                platformName: 'android',
                deviceName: 'device',
                app: '/root/tmp/ApiDemos-debug.apk',
                appActivity: '.ApiDemos',
                appPackage: 'io.appium.android.apis', 
                takesScreenshot: false,
            },
        }

        driver = Appium::Driver.new(desired_caps, true)
        Appium.promote_appium_methods self.class
        driver.start_driver.manage.timeouts.implicit_wait = 20
    end
    
    def teardown
        driver_quit
    end

    def test_sample
        puts "hello"
    end
end

これをえいっと実行すると……あれ? 怒られるよ?

Loaded suite /home/naruhiko/playground/ruby-appium/test
Started
E
===============================================================================
Error: test_sample(SimpleTest): RuntimeError: App doesn't exist. /root/tmp/ApiDemos-debug.apk
/home/naruhiko/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/appium_lib-9.14.3/lib/appium_lib/driver.rb:400:in `absolute_app_path'
/home/naruhiko/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/appium_lib-9.14.3/lib/appium_lib/driver.rb:240:in `set_app_path'
/home/naruhiko/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/appium_lib-9.14.3/lib/appium_lib/driver.rb:176:in `initialize'
/home/naruhiko/playground/ruby-appium/test.rb:17:in `new'
/home/naruhiko/playground/ruby-appium/test.rb:17:in `setup'
===============================================================================
(略)

ん? App doesn't existとはなんぞ?

$ docker exec -it android-container ls /root/tmp 
ApiDemos-debug.apk  Gemfile  Gemfile.lock  test.rb

あるじゃん……??

Appium Desktopで確認

じゃあAppium Desktopでもやってみましょう。Device Capabilitesはこんなふう。

{
  "platformName": "android",
  "deviceName": "device",
  "takesScreenshot": false,
  "app": "/root/tmp/ApiDemos-debug.apk",
  "appActivity": ".ApiDemos",
  "appPackage": "io.appium.android.apis"
}

で、セッションを起こすと……。

f:id:naruoga:20180713230647p:plain

起動するじゃん!

スクリプトから起動してみる - 単なるオタンコでしたの巻

で、ここで冷静になってデバッグメッセージ見ると(再掲)、

Loaded suite /home/naruhiko/playground/ruby-appium/test
Started
E
===============================================================================
Error: test_sample(SimpleTest): RuntimeError: App doesn't exist. /root/tmp/ApiDemos-debug.apk
/home/naruhiko/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/appium_lib-9.14.3/lib/appium_lib/driver.rb:400:in `absolute_app_path'
/home/naruhiko/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/appium_lib-9.14.3/lib/appium_lib/driver.rb:240:in `set_app_path'
/home/naruhiko/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/appium_lib-9.14.3/lib/appium_lib/driver.rb:176:in `initialize'
/home/naruhiko/playground/ruby-appium/test.rb:17:in `new'
/home/naruhiko/playground/ruby-appium/test.rb:17:in `setup'
===============================================================================
(略)

これRubyの世界でエラーになってるじゃないですか。

と、いうことは、Rubyスクリプトを動かしてる環境に /root/tmp/ApiDemos-debug.apk がないよということですね。そりゃそうだ、そんなファイルないもの。

うーんDevice Capabilitesのapp:はサーバ側に渡すものなので、スクリプト側で存在チェックされたらダメじゃん。バグか? いやいやそんなことはないと思うけど……と、デバッガ*11 で追っかけたら、appium_lib/driver.rb のここだ。

    def self.absolute_app_path(opts)
      raise 'opts must be a hash' unless opts.is_a? Hash
      caps            = opts[:caps] || {}
      appium_lib_opts = opts[:appium_lib] || {}
      server_url      = appium_lib_opts.fetch :server_url, false

      app_path        = caps[:app]
      raise 'absolute_app_path invoked and app is not set!' if app_path.nil? || app_path.empty?
      # may be absolute path to file on remote server.
      # if the file is on the remote server then we can't check if it exists
      return app_path if server_url
(略……以下いろんなシチュエーションのアプリ存在チェックが続く)

変数 server_url がセットされてたら app_path (Desired Capabiltiesの app: に渡した値)のチェックをせずそのままサーバーに投げてくれるってことね。っていうか、このメソッドのコメントに説明書いてありますね。

    # Converts app_path to an absolute path.
    #
    # opts is the full options hash (caps and appium_lib). If server_url is set
    # then the app path is used as is.
    #
    # if app isn't set then an error is raised.
    #
    # @return [String] APP_PATH as an absolute path

いや、そもそも、バックトレースに答え書いてあったじゃないか、デバッガ持ち出すまでもなかった……頭悪すぎです。

ということで、スクリプト側でAppiumサーバのURLセットしてなかったのが悪いと。今回はローカルでAppiumサーバーを立ててるときと同じく localhost:4723 でlistenしてたのでサーバーURL指定しなくてもスクリプトそのものは動いたんですが、Docker内で動いてるAppiumサーバーはホストから見たらリモートなのでちゃんと指定しなきゃいけない。

ということで、desired_capsappium_lib も渡すようにして、そこで server_url を指定するようにしたら動くようになりましたと、そういうわけです。

        desired_caps = {
            caps: {
                platformName: 'android',
                deviceName: 'device',
                app: '/root/tmp/ApiDemos-debug.apk',
                appActivity: '.ApiDemos',
                appPackage: 'io.appium.android.apis', 
                takesScreenshot: false,
            },
            appium_lib: {
                server_url: 'http://localhost:4723/wd/hub'
            }

いやーお恥ずかしい。

もしこの間抜けがどなたかのお役に立てば幸いです。

*1:自動テストがprofessionであって、自動がつかないテストについては不勉強の至りです……。

*2:高田馬場ゲーセンミカドの動画がお気に入りです。

*3:Selenideいいですよねえ。機能も実装も素敵。もしJavaで素のWebDriver使って苦労してるならぜひ試してみましょう。

*4:言語系パッケージシステムのライフタイムと、Linuxディストリビューションのそれとは大きく違うのでそれはしょうがないですよね。

*5:というか、待ちきれなくて寝てしまいました。うちはあんまりネットワーク太くないので……。

*6:公式サイト参照。

*7:AppiumのRubyサンプルコード から拝借。

*8:こいつの意味は続きがあればそこで書くかも。さしあたりは公式読んでください。

*9:この活動の一環として将来は作ってみたいなとは思ってますけど……いつになるかは不明。

*10:というか一般にAppImageは便利。LibreOfficeとかでも。

*11:横ネタですけどRuby開発1億年ぶりぐらいなのですが、Visual Studio Code + ruby-debug-ide Gemでソースコードレベルデバッグが便利すぎて鼻血出た。このネタはRuby開発してる人なら当たり前すぎるので今日は割愛しますが、気が向いたら書きます。