2011年4月17日日曜日

JUnitでWebサービスのモックを作る

ソーシャル系のアプリやほかのWebサービスと連動するアプリの単体テストをする場合、
対向するWebサービスをどうしても利用しないとテストできないようなケースが発生する。

たとえば、Twitterアプリを作成している場合など。

Twitterのようなすでに動いているサービスが対向するwebサービスであれば、
テスト用のアカウントを作成しておいて、
そこにテスト用のTweetを入れておけばよいのではあるが、
しかしエンタープライズな場合だと、
そのような準備をできないこともある。

SIerの場合、後の工程(つまり総合試験)にそれらのテストを持って行って、
人月投入するというパターンがあるが、それはそれで、非常にまずい。

そんなわけで、手軽にWebサーバーを構築したいのだが、
tomcatにサーバーをまた立てて、モックのアプリサービスを書いて、
云々というのも非常に面倒臭い。

そこで登場するのがJettyという軽量Webサーバーです。
これはJUnitでもサーバーを立ち上げられる優れもので、テスト用にも利用出来るなかなか優れたういやつです。
ちなみに、現在はバージョン8まで登場しているそうですが、本稿で使用するのは7.3.1です。



さて、早速コードをと言いたいところですが、
今回のサンプルは先に仕様の説明から。
  • Webサーバーはlocalhostにポート3000で立てます。
  • どのようなリクエストが来ても、レスポンスコード200 : OKを返します。
  • コンテンツタイプは"application/json"を返します。
  • メッセージ部分は{"test_result" : "ok"}というjsonが設定されます。

では、早速JUnitのテストコード@BeforeClass@AfterClassからどうぞ。


public class HttpGetResourceTest {

    private static final String TEST_RESULT = "{\"test_result\" : \"ok\"}";

    private static final int PORT_NUMBER = 3000;

    private static final String LOCAL_HOST = "localhost";

    private static Server server;

    /**
     * サーバーを起動する。
     */
    @BeforeClass
    public static void setUpBeforeClass() throws Exception {
        InetSocketAddress inet = new InetSocketAddress(LOCAL_HOST, PORT_NUMBER);
        server = new Server(inet);
        
        // 後述するHandlerの実装。
        Handler handler = new RequestHandlerImpl();
        server.setHandler(handler);
        
        server.start();
    }

    /**
     * サーバーを終了する。
     */
    @AfterClass
    public static void tearDownAfterClass() throws Exception {
        server.stop();
        server.destroy();
    }



@BeforeClassではサーバーを起動して、@AfterClassはサーバーをシャットダウンします。

一応このテストクラスが実行されている間は、サーバーはローカルホスト3000番で起動します。

実際にどのような処理がなされるかは、このテストの内部クラスRequestHandlerImplに記述します。

では、RequestHandlerImplをどうぞ。


    private static class RequestHandlerImpl extends AbstractHandler {

        @Override
        public void handle(String target, Request baseRequest,
                HttpServletRequest request, HttpServletResponse response)
                throws IOException, ServletException {
            
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json");
            response.setStatus(HttpServletResponse.SC_OK);
            
            Writer writer = response.getWriter();
            writer.write(TEST_RESULT);
            
            HttpConnection connection =
                HttpConnection.getCurrentConnection();
            Request req = connection.getRequest();
            req.setHandled(true);
        }
    }


ここでは本当に先程の仕様通りに
  • どのようなリクエストが来ても、レスポンスコード200 : OKを返します。
  • コンテンツタイプは"application/json"を返します。
  • メッセージ部分は{"test_result" : "ok"}というjsonが設定されます。

となっています。
あまり説明いらないですね。

では、サンプルのテストコードを書いてみましょう。
なお、このサンプルではApache HttpComponentsを利用しています。


    @Test
    public void testIsServerReady() throws Exception {
        URI uri = URIUtils.createURI("http",
                LOCAL_HOST, PORT_NUMBER,
                "/search",
                "user=mike_neck&start=10",
                null);
        HttpGet hGet = new HttpGet(uri);
        HttpClient client = new DefaultHttpClient();
        HttpResponse res = client.execute(hGet);
        
        HttpEntity entity = res.getEntity();
        
        if(entity != null){
            StringBuilder builder = null;
            InputStream input = null;
            try {
                input = entity.getContent();
                InputStreamReader reader =
                        new InputStreamReader(input, "UTF-8");
                builder = new StringBuilder();
                while(reader.ready())
                    builder.append((char)reader.read());
            }finally{
                input.close();
            }
            assertThat("testIsServerReady",
                builder.toString(),
                is(TEST_RESULT));
        }else{
            fail();
        }
    }


適当なクエリーをつけてGetで呼び出しました。
ここの部分は実際のクラスを入れておく部分ですね。



と、まあこんな感じなわけですが、
多少依存性がありますので、その情報も書いておきます。

必要なライブラリーは
commons-logging.jar
jetty-continuation.jar
jetty-http.jar
jetty-io.jar
jetty-server.jar
jetty-util.jar
servlet-api.jar

です。

皆さん、Web連携アプリのテストもしっかりとやりましょう。

2011年4月5日火曜日

AnnotationとenumとInterface

プログラミングを始めると、いつもメタプログラミングなことを始めてしまって、
成果がよくないことがあるのですが…

これもその一例…

でも、意外とやってみて使えそうな気がしたのでメモです。

Javaのenumは何気にInterfaceの実装を記述することができて、
switch - case文でやっていることをそのままenumにさせることが可能だったりする。

以下はswitch文を使った例

public enum Fruit {
    APPLE, ORANGE
}

public class DisplayFruit {
    public void printFruitName(Fruit fruit){
        switch(fruit){
        case APPLE:
            System.out.println("This is an apple.");
            break;
        case ORANGE:
            System.out.println("This is an orange.");
            break;
        }
    }
}


このDisplayFruitクラスのユーザーはprintFruitNameメソッドに
enum Fruitを引数として渡して、
その結果としてそれぞれのフルーツの名前が表示される。

という仕様になっているわけですが、
これまたテストするのが面倒くさいなと思うわけですよ。

テストケースは二つで、
  • FruitAPPLEだった場合、This is an apple.と表示されること。
  • FruitORANGEだった場合、This is an orange.と表示されること。

これって、どうやんの?と思う。
いや、すくなくともオレだけは。

というわけで、
Javaのenumは何気にInterfaceの実装を記述することができる
を活用してみる。

まずはインターフェース。

public interface FruitName {
    public String getName();
}


で、次にenum。

public enum Fruit implements FruitName {
    APPLE{
        @Override
        public String getName(){
            return "This is an apple";
        }
    },
    ORANGE{
        @Override
        public String getName(){
            return "This is an orange";
        }
    }
}


こうすると、DisplayFruitはこんなになる。

public class DisplayFruit {
    public void printFruitName(Fruit fruit){
        System.out.println(fruit.getName());
    }
}


これでDisplayFruitのテストはほぼ無くなる。

で、最後にテストをする余地があるとしたら、それはDisplayFruitに対してのテストではなく、
Fruitに対するテストだけになる。

テストコードはこんな感じです。

public class FruitTest{
    @Test
    public void testGetName(){
        assertThat(Fruit.APPLE.getName(), is("This is an apple."));
        assertThat(Fruit.APPLE.getName(), is("This is an orange."));
    }
}


ただし、どっちのほうが性能が良いんだ?とかいう野暮な質問はオレにはしないでくれよ。

…答えられない人ノ

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
です。