Locked History Actions

Android/JUnit

テスト環境について

Androidには立派なテスト環境があり、「これだけやっとけば大丈夫だろ」と言わんばかりのものらしいが、これは間違い! 。。。いや間違いではないのだが、主軸にしてはならない。

Eclipseを開発環境とするならば、Eclipse上でささっとテストができるべきである。 これがまず第一。その上で、テストしにくいGUI部分をこの環境で行うのなら完璧というところだろう。

わざわざテスト用のアプリを作成し、それを決して速いとは言えないエミュレータにインストールし、それからもろもろのテスト操作を行うというのでは、 嫌になること請け合いである。

テストは楽しくなければならないのである。

GUIとビジネスロジックを分割する

Android云々、テスト環境云々以前に、まずこの技術を習得しなければならない。 そもそもGUIは、どういう方法をとってもテストしにくいものなのだから、GUIコードとビジネスロジックコードが完全に分割できるように し、個別のテストが可能でなければならない。

「Androidのテスト環境」にいくら習熟しようともこの点を習得することはできない。

世にあるあまたのサンプルプログラムは、この点を無視したものが非常に多い。 すなわち、プレゼンテーションとビジネスロジックがごちゃまぜなのである。 そこを認識しているか認識していないかの違いで、技術者としての力量がわかるものである。

さらに、別途記述しているが、特にリソースを使ったGUIを作成した場合、それをjarライブラリとしてまとめることはできないし、 そうであれば簡単に使いまわすことができない。これがしたければ、以下のいずれか、あるいは両方の方策が必要になる。

  • GUIとビジネスロジックを完全に切り離す
  • GUI構築においてリソースを一切使用しない

ADTプラグインを何とかする

Eclipse上のADTプラグインは便利なものだが、一つ困ったことがある。 プロジェクトの実行に必要なものを有無をいわさず取り込んでapkにしてしまうことである。

これがなぜ困るのかと言えば、安易にテスト用のコードを混ぜることができないから。 これがゆえにAndroid「純正」のテストは、わざわざ別プロジェクトを作成することになっている。

この環境は、同じプロジェクトにテスト用コードがあるなどとは夢にも思っていないのである。 これでは(私のやり方としては)困るのである。

方策は二つある。

  • ADTプラグインを使わない。プロジェクトを普通のJavaプロジェクトとする。
  • ADTプラグインを使うが、そのapkビルド機能は使わない。

しかし、いずれにしてもantは利用しなければならないので、結局前者の方が楽ではある。

次はbuild.xmlをどこからか調達することが課題である。

build.xmlサンプル

ググってみると、build.xmlのサンプルもあるにはあるが、対応バージョンが古く、現在のSDK(2.2)ではそのままでは使用できない。 これらを参考にして、修正したものが以下である。

<?xml version="1.0" encoding="UTF-8"?>
<!--
注意事項

1. ANDROID_SDK というプロパティはantの環境設定で指定すること。

2. 本来antではOSの違いを吸収してくれ、常にパスセパレータを'/'と記述してよいのだが、
ここでは、Androidのツールに与えるパスもいっしょくたに扱っているため、OSの正式なパスセパレータを
指定できるようにしている。

3. src, bin, gen, assets, res等のフォルダはEclipse ADTプラグインが自動生成するものに
あわせてある。libフォルダは、このプロジェクトに必要なライブラリjarを入れておく場所。

4. src, genはJavaソースの格納されるフォルダであるが、すべての.javaファイルをコンパイル対象と
するのではなく、*Test.javaは除去している。これらはJUnitテストである。つまり、プロダクト用コードと
そのユニットテスト用コードが同じソースフォルダで隣どうしに並んでいることを前提にしている。

-->
<project default="debug">
        
        <!-- OS依存 -->
        <property name="FS" value="\"/>
  <property name="BASE_DIR" value="${basedir}"/>
        <property name="SCR_EXT" value=".bat"/>
        <property name="EXE_EXT" value=".exe"/>
        
        <!-- このアプリケーションの定義  -->
        <property name="PROJECT_NAME" value="sample"/>
        <property name="API_LEVEL" value="8"/>
  <property name="APP_PACKAGE" value="com.beust.android.translate"/>
        
  <!-- Android SDKの場所 -->
        <!--
        <property name="ANDROID_SDK" value="c:\... これはantの環境設定で指定すること"/>
        -->
  <property name="ANDROID_TOOLS" value="${ANDROID_SDK}${FS}tools"/>
        <property name="ANDROID_API" value="${ANDROID_SDK}${FS}platforms${FS}android-${API_LEVEL}"/>
  <property name="ANDROID_JAR" value="${ANDROID_API}${FS}android.jar" />
        <property name="ANDROID_API_TOOLS" value="${ANDROID_API}${FS}tools"/>
  <property name="ANDROID_FRAMEWORK" value="${ANDROID_API_TOOLS}${FS}framework.aidl" />

  <!-- ツール -->
  <property name="AAPT_TOOL" value="${ANDROID_API_TOOLS}${FS}aapt${EXE_EXT}"/>
  <property name="AIDL_TOOL" value="${ANDROID_API_TOOLS}${FS}aidl${EXE_EXT}"/>
  <property name="ADB_TOOL" value="${ANDROID_TOOLS}${FS}adb${EXE_EXT}"/>
  <property name="DX_TOOL" value="${ANDROID_API_TOOLS}${FS}dx${SCR_EXT}"/>
  <property name="APKBUILDER_TOOL" value="${ANDROID_TOOLS}${FS}apkbuilder${SCR_EXT}"/>
        
  <!-- Eclipse ADTによるディレクトリ -->
  <property name="BIN_DIR" value="${BASE_DIR}${FS}bin" />
  <property name="BIN_CLASSES_DIR" value="${BIN_DIR}${FS}classes" />
  <property name="GEN_DIR" value="${BASE_DIR}${FS}gen" />       
  <property name="SRC_DIR" value="${BASE_DIR}${FS}src" />
  <property name="ASSETS_DIR" value="${BASE_DIR}${FS}assets" />
  <property name="RES_DIR" value="${BASE_DIR}${FS}res" />

        <!-- 必要なライブラリをいれておくディレクトリ -->
        <property name="LIB_DIR" value="${BASE_DIR}${FS}lib"/>
        
  <!-- 生成物 -->
  <property name="DEX_FILE" value="${BIN_DIR}${FS}classes.dex" />
  <property name="RES_PACKAGE" value="${BIN_DIR}${FS}${PROJECT_NAME}.ap_" />
  <property name="APK_DEBUG" value="${BIN_DIR}${FS}${PROJECT_NAME}-debug.apk" />
  <property name="APK_UNSIGNED" value="${BIN_DIR}${FS}${PROJECT_NAME}-unsigned.apk" />

  <!-- 必要なディレクトリが未作成であれば作成する -->
  <target name="dirs">
      <echo>Creating output directories if needed...</echo>
      <mkdir dir="${BIN_DIR}" />
      <mkdir dir="${BIN_CLASSES_DIR}" />
  </target>

  <!-- プロジェクトのリソースからR.javaファイルを作成する -->
  <target name="resource-src" depends="dirs">
      <echo>Generating R.java / Manifest.java from the resources...</echo>
      <exec executable="${AAPT_TOOL}" failonerror="true">
          <arg value="package" />
          <arg value="-m" />
          <arg value="-J" />
          <arg value="${GEN_DIR}" />
          <arg value="-M" />
          <arg value="AndroidManifest.xml" />
          <arg value="-S" />
          <arg value="${RES_DIR}" />
          <arg value="-I" />
          <arg value="${ANDROID_JAR}" />
      </exec>
  </target>

  <!-- .aidlファイルからJavaクラスを作成する -->
  <target name="aidl" depends="dirs">
      <echo>Compiling aidl files into Java classes...</echo>
      <apply executable="${AIDL_TOOL}" failonerror="true">
          <arg value="-p${ANDROID_FRAMEWORK}" />
          <arg value="-I${SRC_DIR}" />
          <fileset dir="${SRC_DIR}">
              <include name="**/*.aidl"/>
          </fileset>
      </apply>
  </target>

  <!-- Javaソースをコンパイルする。 -->
  <target name="compile" depends="dirs, resource-src, aidl">
    <javac encoding="UTF-8" target="1.6" debug="true" extdirs=""
      destdir="${BIN_CLASSES_DIR}"
      bootclasspath="${ANDROID_JAR}"
      srcdir="${SRC_DIR};${GEN_DIR}">
       <include name="**/*.java"/>
         <exclude name="**/*Test.java"/>                
       <classpath>
         <fileset dir="${LIB_DIR}" includes="*.jar"/>
       </classpath>
     </javac>
  </target>

  <!-- .classファイルを.dexファイルに変換する -->
  <target name="dex" depends="compile">
      <echo>Converting compiled files and external libraries into ${DEX_FILE}...</echo>
      <apply executable="${DX_TOOL}" failonerror="true" parallel="true">
          <arg value="--dex" />
          <arg value="--output=${DEX_FILE}" />
          <arg path="${BIN_CLASSES_DIR}" />
          <fileset dir="${LIB_DIR}" includes="*.jar"/>
      </apply>
  </target>

  <!-- リソースとアセットをパッケージファイルに入れる。 -->
  <target name="package-res">
      <echo>Packaging resources and assets...</echo>
      <exec executable="${AAPT_TOOL}" failonerror="true">
          <arg value="package" />
          <arg value="-f" />
          <arg value="-M" />
          <arg value="AndroidManifest.xml" />
          <arg value="-S" />
          <arg value="${RES_DIR}" />
          <arg value="-A" />
          <arg value="${ASSETS_DIR}" />
          <arg value="-I" />
          <arg value="${ANDROID_JAR}" />
          <arg value="-F" />
          <arg value="${RES_PACKAGE}" />
      </exec>
  </target>

  <!-- デバッグキーで署名したapkを作成する。
         そもそもapkをインストールするには、署名を行わなければならないのだが、ここでは専用の
             デバッグキーなるもので署名するらしい。らしいというのは、ドキュメントが見つからないから。
         androidの開発環境では、肝心要の部分のマニュアルがみつからない。
        -->
  <target name="debug" depends="dex, package-res">
      <echo>Packaging ${APK_DEBUG}, and signing it with a debug key...</echo>
      <exec executable="${APKBUILDER_TOOL}" failonerror="true">
          <arg value="${APK_DEBUG}" />
          <arg value="-z" />
          <arg value="${RES_PACKAGE}" />
          <arg value="-f" />
          <arg value="${DEX_FILE}" />
          <arg value="-rf" />
          <arg value="${SRC_DIR}" />
          <arg value="-rj" />
          <arg value="${LIB_DIR}" />
      </exec>
  </target>

  <!-- 未署名のapkファイルを作成する。リリース時には署名しなければならない。 -->
  <target name="release" depends="dex, package-res">
      <echo>Packaging ${APK_UNSIGNED} for release...</echo>
      <exec executable="${APKBUILDER_TOOL}" failonerror="true">
          <arg value="${APK_UNSIGNED}" />
          <arg value="-u" />
          <arg value="-z" />
          <arg value="${RES_PACKAGE}" />
          <arg value="-f" />
          <arg value="${DEX_FILE}" />
          <arg value="-rf" />
          <arg value="${SRC_DIR}" />
          <arg value="-rj" />
          <arg value="${LIB_DIR}" />
      </exec>
      <echo>It will need to be signed with jarsigner before being published.</echo>
  </target>

        <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        デフォルトのエミュレータへのインストール
        ただ一つのエミュレータが起動中でなければならない。
        +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -->
        
  <!-- インストール -->
  <target name="install" depends="debug">
      <echo>Installing ${APK_DEBUG} onto default emulator...</echo>
      <exec executable="${ADB_TOOL}" failonerror="true">
          <arg value="install" />
          <arg value="${APK_DEBUG}" />
      </exec>
  </target>

        <!-- 再インストール -->
  <target name="reinstall" depends="debug">
      <echo>Installing ${APK_DEBUG} onto default emulator...</echo>
      <exec executable="${ADB_TOOL}" failonerror="true">
          <arg value="install" />
          <arg value="-r" />
          <arg value="${APK_DEBUG}" />
      </exec>
  </target>

  <!-- アンインストール -->
  <target name="uninstall">
      <echo>Uninstalling ${APP_PACKAGE} from the default emulator...</echo>
      <exec executable="${ADB_TOOL}" failonerror="true">
          <arg value="uninstall" />
          <arg value="${APP_PACKAGE}" />
      </exec>
  </target>

        <!-- +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        クリーン
        ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -->
        <target name="clean">
                <delete dir="${BIN_DIR}"/>
  </target>
        
</project>

ログ出力に注意

Androidでのログ出力はandroid.util.Logクラスのstaticメソッドで行うことになっているが、これまた多くのウェブサイトにて単純にデバッグ用ログの出力を

  Log.d(tag, message);

などと平然と記述している例が多いのだが、これは間違い!

  if (Log.isLoggable(tag, Log.DEBUG) {
    Log.d(tag, message);
  }

としなければならない。

参考サイトとしては以下。 http://hackmylife.net/2010/09/androidutillog.html

ただし、毎回毎回このようなコードを書くのが苦痛になるのはあたりまえである。 なんらかの自作コードでAndroidのロギングユーティリティの呼び出しをラップするのが当然だろう。

Android-APIに注意

AndroidのAPIを使わなければAndroid用アプリは当然組めないのだが、逆説的であるがこれを使ってはならないこれらのAPIをラップしたものを使用するべきである。

前項のロギングユーティリティが良い例だが、これらはstaticメソッドであるため、これを直接使用してしまうと、どうあってもテストはエミュレータ上で行うしかなくなってしまう(あくまで一般的な話。これでもテスト可能な特殊な環境もあるらしいが)。

ひどいことにSDKに含まれているAPIすなわちandroid.jarというファイルには中身がない。 これは単にアプリがコンパイルできるようにするためだけのモックでしかないのである。 例えば、android.util.Logクラス逆コンパイルしてみると以下のようなコードになる。

/*    */ package android.util;
/*    */ 
/*    */ public final class Log
/*    */ {
/*    */   public static final int VERBOSE = 2;
/*    */   public static final int DEBUG = 3;
/*    */   public static final int INFO = 4;
/*    */   public static final int WARN = 5;
/*    */   public static final int ERROR = 6;
/*    */   public static final int ASSERT = 7;
/*    */ 
/*    */   Log()
/*    */   {
/*  4 */     throw new RuntimeException("Stub!"); } 
/*    */   public static int v(String tag, String msg) { throw new RuntimeException("Stub!"); } 
/*    */   public static int v(String tag, String msg, Throwable tr) { throw new RuntimeException("Stub!"); } 
/*    */   public static int d(String tag, String msg) { throw new RuntimeException("Stub!"); } 
/*    */   public static int d(String tag, String msg, Throwable tr) { throw new RuntimeException("Stub!"); } 
/*    */   public static int i(String tag, String msg) { throw new RuntimeException("Stub!"); } 
/*    */   public static int i(String tag, String msg, Throwable tr) { throw new RuntimeException("Stub!"); } 
/*    */   public static int w(String tag, String msg) { throw new RuntimeException("Stub!"); } 
/*    */   public static int w(String tag, String msg, Throwable tr) { throw new RuntimeException("Stub!"); } 
/*    */   public static native boolean isLoggable(String paramString, int paramInt);
/*    */ 
/*    */   public static int w(String tag, Throwable tr) { throw new RuntimeException("Stub!"); } 
/*    */   public static int e(String tag, String msg) { throw new RuntimeException("Stub!"); } 
/*    */   public static int e(String tag, String msg, Throwable tr) { throw new RuntimeException("Stub!"); } 
/*    */   public static int wtf(String tag, String msg) { throw new RuntimeException("Stub!"); } 
/*    */   public static int wtf(String tag, Throwable tr) { throw new RuntimeException("Stub!"); } 
/*    */   public static int wtf(String tag, String msg, Throwable tr) { throw new RuntimeException("Stub!"); } 
/*    */   public static String getStackTraceString(Throwable tr) { throw new RuntimeException("Stub!"); } 
/*    */   public static int println(int priority, String tag, String msg) { throw new RuntimeException("Stub!");
/*    */   }
/*    */ }

つまり、何を呼び出しても"Stub!"というメッセージを出力する例外が発生するだけなのだ。 さらにひどいのは、こんなことをするのであれば、せめてモックの内容を変更できるようにしておいてくれればよいものを、 そんな気も無いらしいということ。

つまり、APIを直接呼び出してしまうと、そのプログラムはどうあってもエミュレータか実機の上でしかテストできないのである。 APIを使用してはならないのだ!