NativeDriver を使ってみた
Googleが公開している、Androidのテストフレームワーク。
WebViewではない、ネイティブなアプリケーションのUIテストができる。
nativedriver
で、早速使ってみた。
jarをつくる
jarが配布されているわけではない。なので、まずは上記ページからソースをチェックアウトして、Antでビルドを行う(このあたりちょっとメンドイ)。
以下の2つのjarを使うことになる。
server-standalone.jar
テスト対象のプロジェクトに組み込むもの。clientとはjson形式で通信を行うらしい。
テスト対象アプリを作る
テスト対象がないと試すことができない。APIデモを対象にしてもよいが、せっかくなので久々にAndroidアプリを作ってみた。
Native Driverは、レイアウトを定義するxmlで指定したidを対象に「クリック」や「値の入力」「内容の確認」等を行うので、わかりやすい名前にするのが吉。
こんな感じで作った。
main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:padding="4dip" android:gravity="center_horizontal" android:layout_width="match_parent" android:layout_height="match_parent" android:weightSum="1"> <TextView android:id="@+id/textView1" android:textAppearance="?android:attr/textAppearanceLarge" android:text="0" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="right" android:layout_weight="0.11" android:textSize="40sp"></TextView> <LinearLayout android:layout_width="match_parent" android:id="@+id/linearLayout1" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:id="@+id/linearLayout2" android:layout_height="wrap_content" android:baselineAligned="false" android:weightSum="1"> <Button android:id="@+id/button7" android:text="7" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="0.20"></Button> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button8" android:layout_weight="0.20" android:text="8"></Button> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button9" android:layout_weight="0.20" android:text="9"></Button> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button_times" android:layout_weight="0.20" android:text="×"></Button> </LinearLayout> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:id="@+id/linearLayout3" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:id="@+id/linearLayout6" android:layout_height="wrap_content" android:weightSum="1"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button4" android:layout_weight="0.20" android:text="4"></Button> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button5" android:text="5" android:layout_weight="0.20"></Button> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button6" android:layout_weight="0.20" android:text="6"></Button> <Button android:layout_height="wrap_content" android:id="@+id/button_minus" android:text="-" android:layout_width="wrap_content" android:layout_weight="0.20"></Button> </LinearLayout> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:id="@+id/linearLayout4" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:id="@+id/linearLayout7" android:layout_height="wrap_content" android:weightSum="1"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button1" android:layout_weight="0.20" android:text="1"></Button> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button2" android:layout_weight="0.20" android:text="2"></Button> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button3" android:layout_weight="0.20" android:text="3"></Button> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button_plus" android:layout_weight="0.20" android:text="+"></Button> </LinearLayout> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:id="@+id/linearLayout5" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:id="@+id/linearLayout8" android:layout_height="wrap_content" android:weightSum="1"> <Button android:layout_height="wrap_content" android:id="@+id/button0" android:text="0" android:layout_width="wrap_content" android:layout_weight="0.20"></Button> <Button android:layout_height="wrap_content" android:id="@+id/button_clear" android:text="C" android:layout_width="29dp" android:layout_weight="0.20"></Button> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button_result" android:layout_weight="0.20" android:text="="></Button> <Button android:layout_height="wrap_content" android:id="@+id/button_div" android:text="÷" android:layout_width="wrap_content" android:layout_weight="0.20"></Button> </LinearLayout> </LinearLayout> </LinearLayout>
ここで忘れないで行わないといけないことがある。
AndroidManifest.xmlの設定
instrumentとパーミッションの設定を行う。
instrumentというのは、JavaのバイトコードとVMの間にインターセプトできる技術らしい。この機能を使い、ボタンのクリックをテストケースから行う。
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="orz.yanagin.android" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="8" /> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".CalcActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> <!-- Without supports-screens, the touch event simulation might not reach the specified view due to coordinate translation. --> <supports-screens android:largeScreens="true" android:normalScreens="true" android:smallScreens="true" android:anyDensity="true" /> <!-- The instrumentation and uses-permissions elements are necessary to enable the Android NativeDriver server in this application. TODO(matvore): add a link to getting started documentation --> <instrumentation android:targetPackage="orz.yanagin.android" android:name="com.google.android.testing.nativedriver.server.ServerInstrumentation" /> <uses-permission android:name="android.permission.DISABLE_KEYGUARD" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> </manifest>
テストを記述するプロジェクトを作る
Androidプロジェクトを作るとき、対になる形でテストプロジェクトを作れるが、上述したようにJUnit3を使いたいのでココは普通のJavaプロジェクトとする。
robotiumも触ってみたが、やはり素のJUnitが使えるのが嬉しい。
こんな感じでテストケースが作れる。
CalcActivityNativedriverTest.java
package orz.yanagin.android.test; import junit.framework.TestCase; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import com.google.android.testing.nativedriver.client.AndroidNativeDriver; import com.google.android.testing.nativedriver.client.AndroidNativeDriverBuilder; public class CalcActivityNativedriverTest extends TestCase { private AndroidNativeDriver driver; @Override protected void setUp() { driver = getDriver(); } @Override protected void tearDown() { driver.quit(); } protected AndroidNativeDriver getDriver() { return new AndroidNativeDriverBuilder().withDefaultServer().build(); } public void testClick0() { driver.startActivity("orz.yanagin.android.CalcActivity"); WebElement textView = driver.findElement(By.id("textView1")); assertEquals("0", textView.getText()); driver.findElement(By.id("button1")).click(); textView = driver.findElement(By.id("textView1")); assertEquals("1", textView.getText()); driver.findElement(By.id("button_plus")).click(); assertEquals("1", textView.getText()); driver.findElement(By.id("button3")).click(); assertEquals("3", textView.getText()); driver.findElement(By.id("button_result")).click(); assertEquals("4", textView.getText()); driver.findElement(By.id("button_clear")).click(); assertEquals("0", textView.getText()); } }
で、いざ実行
その前に、2つやることがある。
instrumentの設定
コマンドプロンプトで以下のコマンドを実行する。
adb shell am instrument [Activityのパッケージ]/com.google.android.testing.nativedriver.server.ServerInstrumentation
Activityのパッケージには、AndroidManifest.xmlの
実際にはこんな感じ
adb shell am instrument com.example.android.apis/com.google.android.testing.nativedriver.server.ServerInstrumentation
このとき以下のようなエラーが発生する場合は、実機/エミュレータにアプリをインストールして上記コマンドを実行すると解消される。
android.util.AndroidExceptioin: NSTRUMENTATION_FAILED:
いよいよ実行
CalcActivityNativedriverTestを普通にJUnitで実行する。
このとき、いくつかハマったのでメモ。
java.net.SocketExceptionが発生する
org.openqa.selenium.WebDriverException: java.net.SocketException: Software caused connection abort: recv failed
こんな感じのエラーが発生。この場合は、先ほど実行したコマンドをたたきなおせばよい。
org.apache.http.conn.HttpHostConnectExceptionが発生する
org.openqa.selenium.WebDriverException: org.apache.http.conn.HttpHostConnectException: Connection to http://localhost:54129 refused
これは、端末を外してしまったときなどに遭遇する。ポートフォワードの設定を再度実行すればよい。
oqa.selenium.WebDriverExceptionが発生する
oqa.selenium.WebDriverException: [テスト対象Activity] in loader dalvik.system.PathClassLorg.openader[/data/app/[Activityのパッケージ]-1.apk:/data/app/[Activityのパッケージ]-1.apk]
最初とは別のプロジェクトで試そうとしたところ発生。最初のプロジェクトでは問題なく実行できる。
エラーメッセージを見ると、クラスローダが期待したapkを呼んでいないっぽい。なので、最初に試したAndroidアプリケーションを削除して再度実行すると、問題なく動いた。かわりに最初のプロジェクトが動かなくなった。
これは仕様なのかは解らないが、これだとCIツールと連携して複数のプロジェクトをテストできないので、ちょっとなんとかして欲しいところ。