Appium、みなさまはご存知でしょうか?
モバイルアプリケーションのテスト自動化を自分で書こうとすると大抵挙げられるもので、ベースはSelenium(これまたWebテストの定番として有名)をモバイルアプリに対応させたようなライブラリです。
ざっくりといえば、PythonかJavaでコードを書いていって、そのプログラムからAppium Serverを経由して、端末に物理的に(もしくはリモートで)接続されている端末を実質的に遠隔操作することができます。
当たり前っちゃ当たり前ですがタップ操作やスクロール操作やフォーム入力なんかもできますし、画面の要素やスクリーンショットを取得したりもできるので、一通りのモバイルテストができちゃいます。
今回は、それをいざ運用レベルに持って行こうとするときに大体ハマるであろうポイントの一つ、
「AppiumServerとの接続が不安定でしんどい」
という問題とどう戦ったかを、Appiumの仕組みにさらっと触れながらお話ししようと思います。
Appiumの仕組みについて
Appiumの中核はAppiumServerという、Node.js ──
(ざっくりいえば、JavaScriptをブラウザではなく自分のパソコンで動かせるようにするやつ)
で動いているソフトウェアです。
Node.jsやXcode,AndroidStudioなどを導入してごにょごにょと設定した上で、お使いのコンピュータでAppiumServerというソフトウェアを起動すると、このサーバはその端末に接続されているAndroidやiPhone端末を認識し、「いつでも遠隔操作できるよ」という状態になります。
この状態でPythonかJavaで、Appiumのライブラリを使って書いたプログラム上で、
driver = webdriver.Remote(
command_executor=executor,
options=self.capabilities_options)
みたいなコードを実行すると、
プログラムと、AppiumServerの間に接続が作られ、あとはプログラム側から追加で「ここをタップせえ」と指示を出してあげればAppiumServerがその指示を受け取って実際にスマホを操作してくれる、というわけです。
element = driver.find_element(by='xpath', value='任意のセレクター')
element.click()
このサンプルではセレクターで指定した要素をタップしてくれるっていうわけです。
しんどいポイントその1
一言で言うなら、このAppiumServerとのコネクション、結構不安定です。
まずデフォルトで、ここの接続のタイムアウトが120秒になっていて、まあ放っておけばまず切れます。
それ自体はcapability、つまり設定集に与えるべき項目の newCommandTimeout あたりに適当に大きな数字を設定しておけばなんとかなるっちゃなるんですが、
それはそれとしても、Macとかで深夜運用とかしてると、まあーUSBの電力供給だの接続のロスだのなんだのでマジですぐ死にます。
そして、AppiumServerと端末の接続がちょっとでも怪しいと、AppiumServerとそれに接続しているプログラムもすぐにセッションが破壊されて、プログラムは例外を吐いて落ちます。
そして、これは再現性が割とないこともよくある。つらい。
しんどいポイントその2
これまたまず一言で言うと、Appiumは要素検索とかでしょっちゅうエラーを吐きます。
内部で何が起きてるのかは把握し切れてないですが、ちょっとしたDOMの更新遅れとか取得ミスとかで、あるはずのロケータ(画面要素を指定する住所みたいなもの)が「ん?ないけど?」みたいな顔して NoSuchElementException を吐きます。
いやあるけど?の気持ちで、もう一回何も条件変えずに回すと、普通に通ったり、よくあります。
そして別のところで引っかかったり。無対策で数百ステップあるテストを回すと、まあ8割くらい完走しないで毎回違うところでこのエラーで落ちるなんて当たり前にあります。
つまり?
ものすごくfreakyでFlakyってことです。
何それ?
フレーキーテスト、Flaky Testとは、コードに変更がないのに、実行するたびに成功したり失敗したりと結果が不安定に変わるテストのことで、ソフトウェア開発において信頼性を低下させる厄介な問題です。
だそうで。freakyについてはまあお気持ちです。
テストを自動化したいのにこうなるなんて本末転倒。
実際まあ何度も回して完走したらオッケーとするって運用もありでしょうが、いざ本当に要素が変わったときに、それを即座に「落ちた」と断言できずに「うーん、通るはずで落ちてるかもしれないからあと何回か回して様子見よう」なんてやっていられないよね、というわけで。(それはそう)
だし、上記のしんどいポイント2つに加えて「本当に要素が変わってリグレッションで落ちている」みたいなケースが実行結果から判別つかないなんて状況は論外でしょう。なんとかせねば。
やったこと
技術的にはそれなりにめんどいことをしました。
技術的にざっと書くとこう
Appiumをラップしているクラスがあるのですが、
そこの __init__ 関数で2つ目のスレッドを立ててそこで死活管理をします。
コネクションを張る関数を用意した上で、ひたすらdriver.get_window_size()をループで叩き続け、もしそこで接続切れたら、メインスレッドの実行をロックしてリコネクションが張れるまでリトライ。指数バックオフを入れてリトライ5回で諦めてエラー吐くように実装。
また、それとは別に、各オペレーションのデコレータにそれぞれの操作でこれまた例外が飛んだら指数バックオフでデフォルト8回だったかな?リトライするように実装。
ここでは、なので、最初は1秒とかから、だんだん伸びてトータル40秒くらいかけて再実行してダメだったら落ちるみたいな実装にしてます。
これを入れてから、Appiumの実行が早すぎてSleep操作を入れて待機する命令がほぼ要らなくなりました。
そんだけ時間掛けて、かつ接続が確実にできてるのに落ちてるなら、まあ多分その要素はないと判断できますね。
抽象的に同じ内容を書くとこう
まず、AppiumServerとの接続を監視し、切れたら再接続するよう管理するプログラムの実行の流れと、
テストコマンドの実行をするプログラムの実行の流れ。
この二つに分けました。
そして、接続管理の方で何かエラーがあったら、テストコマンド実行の方を一時停止して、とにかく再接続を5回時間をあけて頑張るようにプログラムを書きました。
これで大体の接続エラーは潰せます。これでもダメならまず端末接続とかパソコンの調子が根本的に悪いので、どのみち対処が必要っていうアラートとして上げられるわけです。
また、それとは別に、テストコマンド実行でエラーが起きたら即落ちじゃなく、再実行の時間をどんどん伸ばしてって8回リトライするようにしました。
これまた、数回で通ったりすることはよくあるし、トータル40秒くらいかけてもボタンが押せないようなら、かなりの確率でそのプロダクトか、あるいはシステムが根本的に何かミスってる可能性が高そう、と言えるわけです。
で、実際どうか
もちろん完璧ではありませんが、かなり初期対応を絞れるようにはなりました。
実際、この仕組みを実装してからFlakyにテストが落ちることは「なくなり」ました。
この状況で落ちたら、疑いなくロケータがミスってるよね、で対応できるレベルで、なのでそのエラーは再現する状態です。大勝利。
(ただし、Macがロックされてる状態でプログラムが止まる問題は別途残ってますが、これはもうAppiumどうこうっていうよりMacの動きが怪しいと思っているので、今まさに対応中……。)
今後の展開と、まとめ
Appiumによる自動テストを実運用レベルに持っていくには、やはり課題が多くあります。
今回のこういった問題もまさにその一つで、
「スマホを遠隔操作できたすごい!」
から、
「Appiumでテスト自動化をシステムとして運用しています」
まで昇華させるにはやはりこういった問題への泥臭い対応が多く求められ、やりがいがあるところです。
テスト自動化の実装をして終わりではなく、「信頼される自動テスト」として、現実的に手動のテストと垣根なく運用に持っていき、「このリグレッションテストは回ってるものとして僕らは別の効果的なテストに注力できるよね」と信頼されるための……、
そう、信頼される自動テストのためにはこういうところと向き合うのが、大変ですが結局ここが一番効くし、避けて通れない点だと考えています。
おわり&お気持ち
ありがとうございました。外部発信をなかなかしてこなかった身として大変良い機会をいただき、主催のitoさまをはじめ、関わった皆様に改めて感謝を。
なお、直近でMacBook Proにカップ麺を全てぶちまけるというバカをやらかして、投稿が日付を回ってしまったこと、深くお詫び申し上げます………。
再発防止のため、「作業机で上が開いている食品を食べることを二度としない」を誓いとして、この投稿を締めさせていただきます。
ここまでお読みくださり、ありがとうございました。
蛇足
どうすんだよこれから開発が一番捗る年末年始だっつーのに。