2011年4月3日日曜日

AndroidのOnClickListenerをテストしよう

最近、Androidの本、よく売れていますね。

本屋でもJavaのコーナーよりも場所が広かったりします。

で、それらに掲載されているコードに文句をつけるつもりは毛頭ございませんが、

書いてあるのをそのまま参考にすると、テストで痛い目を見ます。

では、まずダメなアプリの例から。


package orz.mikeneck;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import  android.widget.Button;

public class FirstActivity extends Activity {

 /**
  * @see android.app.Activity#onCreate(android.os.Bundle)
  */
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  // Setting layout to a display.
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  // Adding onClickListener to a button.
  Button button = (Button) findViewById(R.id.button_id);
  button.setOnClickListener(

   new View.onClickListener() {
    public void onClick(View v){

     // If this button is pressed, new activity will be launched.
     Intent intent = new Intent(this, NextActivity.class);
     startActivity(intent);
    }
   }
  );
 }
}


ダメであるという理由は、次のテストが実施できないからです。

・ボタンを押したときに、指定したActivityが起動すること

なぜテストが実施できないかというと、

オブジェクトButtonというよりはViewには、
setOnClickListener(android.view.View.OnClickListener)はあっても、
getOnClickListener()がないため、
ボタン(View)を押すという操作をテストから実行することができないからです。

一般的な開発工程のあり方としては、単体テストでバグを摘出するほうが、結合試験以降でバグを抽出することよりもコストが低いと言われています。

結合テストまでこのバグが潜んでしまっていた場合、どのクラスによって発生したのか切り分ける必要があるため、念入りに調査することが必要になります。

プログラマーで勘の良い人はだいたいどの辺にバグがあるのかを検出するのが速いわけですが、誰しもがそうではないわけで、こんな単純なバグが原因だったのかと呆れてしまうこともあったりします。

私もSIに務めているので、そのようなことはよくあったりします。

なんか画面のボタンを押したときに、条件を限定しているわけではないのに、なにか限定された結果が得られて、どのような条件でどのような操作をするとバグが発生するのか、詳細にバグ票に書かなければならないので、結構しんどかったりします。

で、その結果、実はボタンを押したあとの挙動にハードコーディングがしてあったとかよくありました。

…最近は上流のテストはやっていないので、そんなことはなくなったのですが…

話を戻すと、View.onClickListenerはせっかくinterfaceなのだから、それを有効活用しない手はありません。

というわけで、以下がまだましな例。
FirstActivity.java

package orz.mikeneck;

import android.app.Activity;
import android.os.Bundle;
import android.view.View.OnClickListener;
import  android.widget.Button;

public class FirstActivity extends Activity {

 /**
  * @see android.app.Activity#onCreate(android.os.Bundle)
  */
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  // Setting layout to a display.
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  // Getting instance of OnClickListener.
  View.onClickListener listen = new OnClickListenerImpl();
  listener.setPreContext(this);
  listener.setNextContext(NextActivity.class);

  // Adding onClickListener to a button.
  Button button = (Button) findViewById(R.id.button_id);
  button.setOnClickListener(listener);
 }
}


ここで、先程の例ではActivity内にあったFirstActivityの中にあった、
View.onClickListenerの実装が外に出されました。

で、これが外に抽出されたクラスOnClickListenerImpl.javaです。

package orz.mikeneck;

import android.content.Context;
import android.content.Intent;
import android.view.View;
import android.view.View.OnClickListener;

public class OnClickListenerImpl implements OnClickListener {

 /**
  * Previous Context listening user motion. 
  */
 private Context preContext;

 /**
  * Next Context to be launched.
  */
 private Class nextContext;

 /**
  * If the button is pressed, the next Activity will be launched by this method.
  * And, this method can be called by external classes.
  */
 @Override
 public void onClick(View v) {
  Intent intent = new Intent(preContext, nextContext);
  preContext.startActivity(intent);
 }

 public void setPreviousContext(Context preContext) {
  this.preContext = preContext;
 }

 public void setNextContext(Class nextContext) {
  this.nextContext = nextContext;
 }


}


そうすると、これである特定の画面から、次の画面を呼び出すという挙動のテストができるようになります。
以下はテストコード。
OnClickListenerImplTest.java

package orz.mikeneck.test;

import android.content.Intent;
import android.test.mock.MockContext;
import junit.framework.TestCase;
import orz.mikeneck.OnClickListenerImpl;

public class OnClickListenerImplTest extends TestCase {

 private OnClickListenerImpl listener;

 private boolean isTestOnClickPassed;

 @Override
 public void setUp(){
  this.isTestOnClickPassed = false;
  listener = new OnClickListenerImpl();
  PreContext preContext = new PreContext();
  listener.setPreviousContext(preContext);
  listener.setNextContext(NextContext.class);
 }

 /**
  * Click the button(fake).
  */
 public void testOnClick(){
  listener.onClick(null);
  assertTrue(isTestOnClickPassed);
 }

 private class PreContext extends MockContext{

  /**
   * @see android.test.mock.MockContext#startActivity(android.content.Intent)
   */
  @Override
  public void startActivity(Intent intent) {
   super.startActivity(intent);
   isTestOnClickPassed = true;
  }
  
 }

 private class NextContext extends MockContext{ 
 }
}


実はView.onClickListenerのメソッドonClick(View v)は、
パブリックメソッドなので、クラスの外から呼び出すことが可能です。

したがって、テストクラスでは堂々とlistener.onClick(null)と呼び出していますね。



ただし、ここからは私のうっかりミスが発覚します。
これをそのままテスト実行すると、もれなく


java.lang.UnsupportedOperationException
at android.test.mock.MockContext.getPackageName(MockContext.java:101)
at android.content.ComponentName.(ComponentName.java:75)
at android.content.Intent.(Intent.java:2551)
at orz.mikeneck.OnClickListenerImpl.onClick(OnClickListenerImpl.java:52)
at orz.mikeneck.test.OnClickListenerImplTest.setUp(OnClickListenerImplTest.java:28)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:169)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:154)
at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:430)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1447)


とかなります。

う~ん、前々から気にはしていたんですがね、android.test.mock.MockContextは、
どうも暴れん坊大将軍様で、呼び出すメソッドすべてがUnSupportedException
返してくれるという素敵仕様になっております。
したがって、テストを通すまでかなりの道のりを経なくてはならない化物なのでございます。

で、今回はExceptionの発生しているメソッドのオーバーライドもしました。

 private class PreContext extends MockContext{

  /**
   * @see android.test.mock.MockContext#startActivity(android.content.Intent)
   */
  @Override
  public void startActivity(Intent intent) {
   isTestOnClickPassed = true;
  }

  /**
   * Added method.
   * @see android.test.mock.MockContext#getPackageName()
   */
  @Override
  public String getPackageName() {
   return "orz.mikeneck.test";
  }
  
 }


これで、見事にテストが通りました。

というわけで、ある特定のActivityから指定したActivityを起動することが出来ているという確認が取れるようになりました。



はい、妙に長ったらしい解説でしたが、

Activity内部でinterfaceの実装を記述しないこと


が今回言いたかったことです。


書いていたら、22時過ぎてしまった…orz

Androidアプリ用のイメージ

Androidアプリ用のイメージ、結構揃えるのが面倒くさいですね。

アプリを作っていて、凝ったアプリではないのでアイコンもそれほど凝らなくてもいいようなアプリを作りたいときがあります。

そういうときはネット上の素材に頼るのがよいようです。

というわけで、簡単にアプリ用のイメージをまとめてみました。

Open Source Icons -- Linux系のアイコンがいっぱいある。これはかなり使いやすい。LGPLだったり、GPLだったりするので、ライセンス関連を気にする人は要注意。

Button Maker -- 簡単な文字の入ったアイコンをつくるのに適したサイト。自分でキーボードを作るときにも便利ですね。

PhotoShifter -- Windows用ですが、gif形式のファイルをpngに変換したり、リサイズしたりできます。コマンドラインでも利用可能だそうです。


ちなみに、
res/drawable-hdpiのアイコンのサイズは72x72
res/drawable-hdpiのアイコンのサイズは36x36
res/drawable-hdpiのアイコンのサイズは48x48
です。

2011年3月28日月曜日

TinySVMのインストール



YamChaのインストールCaboChaのインストール方法を先日述べましたが、
実は必須のソフトウェアのインストール方法については述べていませんでした。
それが今回の題材である、TinySVMです。

まぁ、英語だらけでよくわからんが、単純にSupport Vector Machinesという学習アルゴリズムの実装ということだそうです。
CaboChaの自己学習機能を利用する場合には必須のソフトウェアです。

では以下、wgetmake installまでのコードです。


$ wget http://chasen.org/~taku/software/TinySVM/src/TinySVM-0.09.tar.gz
$ tar zxvf TinySVM-0.09.tar.gz
$ cd TinySVM-0.09/
$ ./configure
$ make
$ make check
$ sudo make install


これを実行すれば、ちゃんとパスを認識してくれて、その機能が利用出来るようになります。

$ whereis svm_learn
svm_learn: /usr/local/bin/svm_learn


一応必要そうなコンパイル系のソフトウェアを掲載しておきます。
  • g++(バージョンは不明)
  • gcc(バージョンは不明)
  • GNU makeなどのビルド系


以下、参考リンク

TinySVM: Support Vector Machines

2011年3月26日土曜日

JUnit4のTestSuite

すごい、なんか悔しいくらいにすごい。
JUnit4@org.junit.runners.Suite

まずは先にサンプルをどうぞ。


package orz.mikeneck.test;

import org.junit.runner.RunWith;
import org.junit.runners.Suite;

import orz.mikeneck.Test1;
import orz.mikeneck.Test2;
import orz.mikeneck.Test3;

@RunWith(Suite.class)
@Suite.SuiteClasses({
  Test1.class,
  Test2.class,
  Test3.class
})
public class TestSuite{
}


これでTest1、2、3の順番でテストを実行してくれる。

すさまじく、美しい。

悔しいので、少しソースを見てみる。








よくわからなかったので、撤退する。

2011年3月25日金曜日

YamChaのインストール


CaboChaと順番関係が逆になったけど…
念のため、備忘録的に。

YamChaがなにをしているかというと、Support Vector Machinesとかいう学習アルゴリズムを使った、テキストチャンカー。
テキストチャンカーというのは、テキストから字句の塊(チャンク)を取り出す機能と解しておけばいいかと思う。
あまりくわしくはないけど…

というわけで、インストール手順は次のとおりです。

$ wget http://chasen.org/~taku/software/yamcha/src/yamcha-0.33.tar.gz
$ tar zxvf yamcha-0.33.tar.gz
$ cd yamcha-0.33/
$ ./configure


また、ここでmakeしたいのを抑えつつソースを書き換える。

書き換えるのは二つのファイル。
  • src/common.h#include <string.h>を書き加える。
  • libexec/mkdarts.cpp#include <cstdlib>を書き加える。

あとは、お約束のコマンドを打っていけばいいです。


$ make
$ make check
$ sudo make install


これで、パスの確認が出来れば完了。

$ whereis yamcha
yamcha: /usr/local/bin/yamcha


さて、ここまで書いてきたが、YamChaのインストールに必要なライブラリーは以下のとおりです。
  • perl 5.00 以上
  • GNU make(普通のLinuxなどには入っている)
  • TinySVM
  • gcc2.95以上

以下、参考リンク
YamCha: Yet Another Multipurpose CHunk Annotator
RとLinuxと…

2011年3月23日水曜日

CaboChaのインストールwget~make installまで

結構、自然言語解析のセットアップにずっとてこずっていて、苦節5回にして、そろそろCaboChaとMeCabの環境がうまく構築できそうな気配である。

以下、CaboChaのダウンロードからmakeまでのコマンドです。

$ wget http://chasen.org/~taku/software/cabocha/src/cabocha-0.53.tar.gz
$ tar zxvf cabocha-0.53.tar.gz
$ cd cabocha-0.53/
$ ./configure --with-mecab-config=/usr/local/bin/mecab-config --with-yamcha-config=/usr/local/bin/yamcha-config --with-morphological-analyzer=mecab --with-charset=UTF8


ここで、makeしたい気持ちを抑えて、微妙にファイルを修正する。
修正するファイルはsrc/common.hです。
このファイルに#include <string.h>がないので追加してやる必要があります。

ファイル : src/common.hの47行目~

#include <set>
#include <stdexcept>
#include <algorithm>

/** added here **/
#include <string.h>
/** add end **/


/** added here **//** add end **/のあたりを追加する。

これでおもむろにmakeしてやる。
このmakeはかなり時間がかかる。使用しているマシンのスペックにも依存するだろうが、3回目以降のTRIEを構築している最中に10分くらいかかる。
だから、makeコマンドを打ったらコーヒー、紅茶、ココアの類を入れて(人によってはタバコでも吸いに行って)気長に待つことが寛容。


IPA-dicのモデルを読み込んで、YamChaを使用してTRIE構造を構築している時に時間がかかるらしい。

ちなみにYamChaのインストールにもくせがあるので、要注意。というかいつか記事を書きます。


Making Double-Array: 100% |*******************************************| 
Making TRIE        : 100% |*******************************************| 
Done!
rm -f IPA-dep.txtmodel
make[2]: ディレクトリ `/home/mike/src/cabocha-0.53/model' から出ます
Making all in tests
make[2]: ディレクトリ `/home/mike/src/cabocha-0.53/tests' に入ります
make[2]: `all' に対して行うべき事はありません.
make[2]: ディレクトリ `/home/mike/src/cabocha-0.53/tests' から出ます
Making all in doc
make[2]: ディレクトリ `/home/mike/src/cabocha-0.53/doc' に入ります
make[2]: `all' に対して行うべき事はありません.
make[2]: ディレクトリ `/home/mike/src/cabocha-0.53/doc' から出ます
Making all in libexec
make[2]: ディレクトリ `/home/mike/src/cabocha-0.53/libexec' に入ります
make[2]: `all' に対して行うべき事はありません.
make[2]: ディレクトリ `/home/mike/src/cabocha-0.53/libexec' から出ます
Making all in training
make[2]: ディレクトリ `/home/mike/src/cabocha-0.53/training' に入ります
echo "Usage: make CORPUS=Your-Corpus-File MODEL=Model-Prefix-Name train-dep/train-chunk/train-ne"
Usage: make CORPUS=Your-Corpus-File MODEL=Model-Prefix-Name train-dep/train-chunk/train-ne
make[2]: ディレクトリ `/home/mike/src/cabocha-0.53/training' から出ます
make[2]: ディレクトリ `/home/mike/src/cabocha-0.53' に入ります
make[2]: ディレクトリ `/home/mike/src/cabocha-0.53' から出ます
make[1]: ディレクトリ `/home/mike/src/cabocha-0.53' から出ます


これでmake完了。

あとはmake checksudo make installを残すのみ。


$ make check
            Ʊ����-D                        
            �յĸ���-D                      
            ���Բ���---------------------D
                ��̳����-------------------D
        �֥ƥ���ī��¦����-------D         |
                  ����������-D   |         |
                ���������٤Ȥ�-D |         |
                          ������-D         |
                          ��������-------D |
                                ����-D   | |
                                ������---D |
                                  ������-D |
                              ���������פ�-D
                                    �Ҥ٤���
EOS

make checkすると、謎の文字列が現れるが、これはおそらくEUC-JPの文字列をUTF8で表示したパターンの文字化け。これはとりあえず、シカトする。



$ sudo make install


これでおしまい。

では、おもむろにcabochaで遊んでみようと思う。



$ cabocha -f1 -O1 -n1
そろそろCaboChaとMeCabの環境がうまく構築できそうな気配である。
そろそろ ソロソロ そろそろ 副詞-助詞類接続   O
CaboCha CaboCha CaboCha 名詞-一般   O
と ト と 助詞-並立助詞   O
MeCab MeCab MeCab 名詞-一般   O
の ノ の 助詞-連体化   O
環境 カンキョウ 環境 名詞-一般   O
が ガ が 助詞-格助詞-一般   O
うまく ウマク うまい 形容詞-自立 形容詞・アウオ段 連用テ接続 O
構築 コウチク 構築 名詞-サ変接続   O
でき デキ できる 動詞-自立 一段 連用形 O
そう ソウ そう 名詞-接尾-助動詞語幹   O
な ナ だ 助動詞 特殊・ダ 体言接続 O
気配 ケハイ 気配 名詞-一般   O
で デ だ 助動詞 特殊・ダ 連用形 O
ある アル ある 助動詞 五段・ラ行アル 基本形 O
。 。 。 記号-句点   O
EOS
3D対応モデルの任天堂3DSが発売された。
3 3 3 名詞-数   O
D D D 名詞-一般   O
対応 タイオウ 対応 名詞-サ変接続   O
モデル モデル モデル 名詞-一般   O
の ノ の 助詞-連体化   O
任天堂 ニンテンドウ 任天堂 名詞-固有名詞-組織   O
3 3 3 名詞-数   O
DS DS DS 名詞-固有名詞-組織   O
が ガ が 助詞-格助詞-一般   O
発売 ハツバイ 発売 名詞-サ変接続   O
さ サ する 動詞-自立 サ変・スル 未然レル接続 O
れ レ れる 動詞-接尾 一段 連用形 O
た タ た 助動詞 特殊・タ 基本形 O
。 。 。 記号-句点   O
EOS


おお、すばらしい。

2011年3月20日日曜日

取得したクラスがあるインターフェースの実装であることを調べるJUnit4のテスト方法

BuilderFactoryパターンを実装する場合、取得されるクラスが想定通りのクラスであるかをテストしたくなります。
その場合に役立つのがJUnit4のIsInstanceOfです。


たとえば、社員(Staff)インターフェースを実装した二つのクラス(SalesStaffとServiceStaff)があって、それらを取得するためのBuilderFactoryパターンを作ったとします。


…ちなみにデザインパターンの本は持っているけど、詳しくは知らないから、その辺勘弁ね。
…あと、UMLとか書き方知らんので、その辺も勘弁ね。


// Factoryを作る
  StaffFactory factory = new StaffFactory();

  // SalesStaffをインスタンス化
  Staff sales = factory.getInstance("Sales");

  // ServiceStaffをインスタンス化
  Staff service = factory.getInstance("Service");


で、このFactoryのテストを書くとしましょう。

単純には、JUnit3のままでやると、こんな感じのテストになるでしょう。

public class FactoryTest extends TestCase {

  public void testGetInstance(){
    // Test1 : SalesStaffを取得できているかテストする。
    assertTrue( factory.getInstance("Sales")
        instanceof SalesStaff);

    // Test2 : ServiceStaffを取得できているかテストする。
    assertTrue( ServiceStaff.class.isInstance(
        factory.getInstance("Service")) );
  }
}


う~ん、微妙…

Test1のケースは、instanceofという演算子が気にくわないですね。
instanceofはClass同士の関係性について調べるものだから、演算子ではないような気がするんです。
むしろClassObjectのメソッドであるべきだと思うわけですよ。

で、一応Classには、#isInstance(java.lang.Object)というメソッドがあって、それを使ったのがTest2
美しくね~な。
何が美しくないって、Class isInstance Objectって、英語のシンタックスにあっていないのが非常に美しくないのですよ。
Object isInstanceOf Classという形にしたい。

まあ、そこでJUnit4に登場願いますのですよ。
JUnit4で同じようなテストを書くと、次のような感じ。


import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

public class FactoryTest {
  @Test
  public void testGetInstance() {
    // Test3 : SalesStaffを取得できているかJUnit4でテストする。
    assertThat( factory.getInstance("Sales"),
      is(instanceOf(SalesStaff.class)));
  }
}


これはナチュラルに英語のシンタックスとあっている。
やはり名前重要!(きのこ本のまつもとゆきひろさんの記事のタイトル)

ちなみに、下記のページを参考にしました。
Android JUnit Testでhamcrestをつかう