Proxyによるモックインスタンスの生成

何となくな動作確認用として、HttpServletResponseとかのインスタンスを簡単に作れないかなと思ってClass.classとか見て調べたけど結局諦めたのが昨日。

で、丁度以下の記事を見て…

そうする事とツールS2JUnit4,djUnit,EasyMock,JMock,JDave,etc...を生かしてMockコーディング工数を減らしたり効率化とかも行いながら工数もなるべく下げようとします。
私のTDD、そしてこれから。もちろん、t-wadaさんに感謝!

Mock生成のツール群というのが(やっぱり)あるらしいので、EasyMockの中身を参考に作ってみた。

  • 基本的にはjava.lang.reflect.Proxyを使う。「モック」というよりは「代理」。
  • モックの動作はjava.lang.reflect.InvocationHandler#invoke(...)が担当する。今回はコレの実装が中心。
    • 基本的には、プリミティブ型の返却値はそれぞれの「最小値」を返し、それ以外のクラスの返却値はnullを返すようにした。
    • メソッドに対する返却値の定義 "MyInvocationHandler#define(String, Object)" はメソッド名のみをキーとしてるので不十分。パラメータの型もキーにすべき。でもそんなに難しくないよね。
    • MyInvocationHandlerの生成で返却値定義Mapを渡して…とも思ったけど、匿名クラス初期化として書いた方が見た目がいい。と思う。
  • 知らないけど出来ることって、いっぱいあるんだなぁ。

追記

defineを変更。所謂「流れるようなインタフェース」化で読みやすくしてみた。

    define("methodName", "returns value");         //前の記法
    define("methodName").returns("returns value"); //新しい記法

ソース

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class MyMock {
    /**
     * モックを生成。
     */
    public static <T> T createMock(Class<T> mockClass, InvocationHandler handler){
        Class<?> proxyClass = Proxy.getProxyClass(mockClass.getClassLoader(), mockClass);
        try {
            return (T)proxyClass.getConstructor(InvocationHandler.class).newInstance(handler);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * 動作の確認用。
     */
    public static void main(String[] args) throws Exception {
        Foo f = createMock(Foo.class, new MyInvocationHandler(){
            {
                define("stringMethod", "ほげほげ");    //最近はこの書き方が好み。覚えたてだからかww
                define("intMethod").returns(123);      //追記:流れるようなインタフェース化
            }
        });
        
        f.voidMethod();
        System.out.println("f.booleanMethod();"+f.booleanMethod()); //prints "false"
        System.out.println("f.intMethod():"+f.intMethod());         //prints "123"
        System.out.println("f.stringMethod():"+f.stringMethod());   //prints "ほげほげ"
    }
    
    /**
     * 動作の確認用。
     */
    interface Foo {
        public void voidMethod();
        public boolean booleanMethod();
        public int intMethod();
        public String stringMethod();
    }
}


class MyInvocationHandler implements InvocationHandler{
    /** 返却値定義Map */
    public final Map<String, Object> returnMap = new HashMap<String, Object>();
    
    /**
     * プリミティブ型のリターン値を列挙
     */
    public enum Primitives{
        $boolean(false),
        $char(Character.MIN_SURROGATE), 
        $byte(Byte.MIN_VALUE), 
        $short(Short.MIN_VALUE), 
        $int(Integer.MIN_VALUE), 
        $long(Long.MIN_VALUE), 
        $float(Float.MIN_VALUE), 
        $double(Double.MIN_VALUE), 
        $void(null),
        ;
        public final Object $default;
        private Primitives(Object $default){
            this.$default = $default;
        }
        public static Primitives $valueOf(Class<?> type){
            return Primitives.valueOf("$"+type.getName());
        }
    }
    
    /**
     * 指定した名前のmethod名の場合、value値を返却するように定義する。
     */
    public void define(String method, Object value){
        this.returnMap.put(method, value);
    }

    //追記
    public Define define(String method){
        return new Define(method);
    }
    
    public class Define {
        public final String method;
        public Define(String method) {
            this.method = method;
        }
        public void returns(Object value){
            returnMap.put(method, value);
        }
    }

    
    /**
     * 指定したmethodの返却値がdefineされている場合はその値、
     * そうではない場合はプリミティブ型の場合はPrimitivesでのdefault値、
     * いずれでもない場合はnullを返却する。
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(returnMap.containsKey(method.getName())) {
            return returnMap.get(method.getName());
        }
        
        Class<?> returnType = method.getReturnType();
        if(returnType.isPrimitive()) {
            return Primitives.$valueOf(returnType).$default;
        } else {
            return null;
        }
    }
}