ものがたり(旧)

atsushieno.hatenablog.com に続く

Google Native Driverの.NET portを作った (or: Androidのテストの仕組み)

表題のブツそのものに興味があって、それ以外はどうでもいいという人は、適当に後ろの方にあるリンクを拾って見てもらいたい。

一昨日、うちのタスクリストのようなものを眺めていて、Mono for Androidにもテストの仕組みが必要だろうと思って、半ば興味本位でAndroidのテストの仕組みを探っていた。Mono for Androidには、まだandroid.testのバインディング以外にユニットテストフレームワークが存在しない(し、これはJUnitのテストのannotationsをマッピングできない関係で、利用できるとは言い難い)。NUnitは動くかもしれないけど、それだけでは足りない。以下少しずつ説明していく。

Androidのテストの仕組みはどうなっているのか

developer.android.comにあるTesting Fundamentalsにも、大まかな話は載っているので、それを理解している人には必要ないかもしれないけど。

Androidの開発は、多くの人はEclipse ADTを使って行っていると思うし、Androidユニットテストを書いている人は多くがJUnitを使っていると思う(想像)。アプリケーションの中にテスト用に android.test.ActivityInstrumentationTestCase2 から派生したクラスを追加して、ADTの機能を使って "Run As" (あるいは "Debug As" )で "Android JUnit Test" を実行したりしていると思う。

これってどういう仕組みで実現しているんだろう?

この疑問にピンと来ない人もいるかもしれないので、もう少し長く書こう。Androidのクラスライブラリ android.jar の中には、android.test というJUnitベースのテストフレームワークがあって、皆それをテストケースに使っていることになる。そのテストコードが動作するのは、Android実機あるいはエミュレーター(以下これらは target と書くことにする)の上でということになるけど、そこにはJVMは存在しない。存在するのはDalvikであり、実行されているのは、いったんJavaバイトコードから変換されたDalvikのバイトコードだ。でも Android JUnit Test を実行している host は、Eclipseの中で動作するJava環境だ。どうやって別々のVMで動作しているテストの結果を共有しているんだろうか?

この疑問を解くキーポイントとして、JUnitの仕組みを知っておく必要があるかもしれない。僕はJUnitを一度も使ったことがない(!)ので、NUnitからの想像で書いてみる。JUnitにはTestRunnerというクラスがあって、これは渡されたテストを「実行する」存在として抽象化されている。通常は、渡されたテストケースのテストメソッドを適宜実行していくだけで足りるのだけど、もし必要があれば、ここに何らかの処理を挟んだり、そもそも何も実行しなかったりということすらできる。

Eclipse ADTとRemoteAndroidTestRunner

ADTは、このTestRunnerの抽象性を活用している。実のところ、Eclipse JDTに、今回行っているような「リモート環境で動作するテスト」を実行するためのorg.eclipse.jdt.internal.junit.runner.RemoteTestRunnerというクラスが存在している。ADTでは com.android.ide.eclipse.adt.internal.launch.junit.runtime.RemoteAdtTestRunner という長ったらしい名前のクラスがあって、これがリモートにあるTestRunnerとやりとりをしていることになる。RemoteAdtTestRunnerは、内部的にはddmlibというsdk用のライブラリに含まれるcom.android.ddmlib.testrunner.RemoteAndroidTestRunnerというクラスを利用している。

RemoteAndroidTestRunnerは、そんなに難しいことをしているわけではない。AndroidにはadbというUSB debuggingなどで使われるコンソールツールがあり、これでも利用できるshellという機能を利用している。shellで "am instrument ..." と命令を送ることで、target側でJUnitを実行して結果を送ってもらうことができるわけだ(instrumentationで送ることが出来るのはクラス名やテスト名であって、テストのコードそのものを送っているわけではない)。

バイス側ではandroid.app.Instrumentationを利用して実装されたandroid.test.InstrumentationTestRunnerが動作して、adb経由で実行を指示されたテストのクラスを名前から探し出して、それを実行している、と思う(target側はほとんど処理フローを追っていないので、この辺は想像だけど、まあ外れてはいないと思う)。

ちなみにAndroidのテストフレームワークにrobotiumというのがあるけど、これを取り込んでCIなど統合的なテストフレームワークを構築しているsciroccoでもこのRemoteAdtTestRunnerを取り込んでいるようだ

また、Android CTSも参考になるはずだと教えてもらったので、ソースを追ってみたのだけど、確かにここにもCTSで独自にリモートテストを実行するためのHostUnitTestRunnerという独自のTestRunnerが用意されている。これが独自に発展している理由はよく分からないけど、内部的には hosttestlibのcom.android.hosttest.DeviceTest を利用していて、これがddmlibを使っていて、結局は同じことを行っているようだ。

Native Driver for Androidの仕組み

さて、以上のようなことを調べて、Eclipse/ADTからtargetのテストがどのように実行できるかを、必要な範囲で把握することが出来た。それで、世にあるAndroid用テストフレームワークはどうなっているんだろうと思って、robotium、robolectric、native driverの3つを眺めてみた。robolectricはandroid.jarの機能の呼び出しをinjectして、あくまでeclipseのローカル側だけでコードを実行しているものだったので、これは僕らがMono for Androidで使うような性質のものではない。

というわけで、robotiumとnative driverが残ったのだけど、robotiumはinstrumentationの仕組みを使用した、target側だけのライブラリなので、RemoteAdtTestRunnerのような仕組みを用意してやらなければならない。一方、native driverは、その実装のコアでもあるselenium WebDriverの仕組みを利用して、host側のクライアントコードと、target側で動作しているサーバコード(中でjettyが動作している!)の命令をやり取りしていて、サーバは内部でinstrumentationの仕組みを利用して、外部からアプリケーションを操作する機能(というか権限)を実現している。

そして、native driverのクライアントのソースを見てみると、コードは実はそれほど多くなく、native driverのクライアントは、seleniumのWebDriver(これもGoogle製)の上に乗っかって、命令を少し拡張したり先のadb shellの機能を呼び出すように実装されたもので、その他の部分はWebDriver上で行われていた。seleniumのWebDriverには、実は.NET版の実装がある。これをベースにC#版のクライアントをJavaからの移植で実装すれば、Native DriverのC#版が作れるのではないかと気が付いた。

というわけで、思い立って移植してみたら、selenium WebDriverにも多少手を加える必要があったものの、割と簡単に実現できた。移植コードは1000行程度しかない。
http://dl.dropbox.com/u/493047/2011/09/nativedriver.net-0.1.tar.bz2

動かしてみたものを画質の悪いカメラで録画してみた例はこちら (or WebM)

使い方

Native Driverは、オリジナルでも実のところ多少ややこしい操作をしないと使えない。adb shellで、Native Driver本家のGettingStartedAndroidにある am instrument と forward を行う必要がある。そして、アプリケーションにserver-standalone.jarを組み込んで、 AndroidManifest.xml にinstrumentation要素を追加する必要がある。(以上は先のアーカイブの中でもREADMEに書いておいた。)

多分これだけやっておけば、後は他のアプリケーションでも使えるようになるはずだと思うのだけど、実のところまだnative driverのサンプルでしか動作確認出来ていない。もちろんMono for Androidで組み込んだものも動かしてみたいと思っている。(今動かそうとしているのだけど、まだハマり中)

総括

というわけで、今回はAndroidのテストがどのように実装されているかを追っかけて、それで得られた知識をもとに、Native Driverを移植してみた。割と簡単に動いてくれた。

たぶんseleniumクライアントがあるrubypythonでも、同じことが同じくらいの作業量で実現できると思う(わたしはseleniumをいじったのもnative driverをいじったのも初めてだったので、予備知識があったわけではない)。