building mock environment using AspectJ

前回から大分時間が空きましたが、少し余裕がでてきたので、GAE/Jでアプリケーションを作って遊んでおります。作っていく中でunit testを書いたりするのですが、どうせ遊びならこのあたりも好き勝手に書いてみたいと思うわけです。

前回のエントリにあるようにGAE/J環境でもAspectJが使えるので、暫くはこれでモックの部分を解決しながら、コンパイル時間が長くなりそうならば他の方法を検討することにしました。AspectJでモックオブジェクトっぽいものを提供する部分については随分昔に一度やっているのですが、もう少し仕組みを広げてみました。

テスト対象となるメソッドがHttpServletRequestに依存しているケースがあります。

protected String getRedirectUrl(HttpServletRequest request){
    String scheme = request.getScheme().toLowerCase();
    String portStr = "";
    int port = request.getServerPort();
    if( !(scheme.equals("http") && (port == 80)) &&
        !(scheme.equals("https") && (port == 443))){
        portStr = ":" + port;
    }
    return 	
        scheme + "://" + 
        request.getServerName() + portStr + 
        request.getContextPath() + redirectTo;
}

モックを使うテストの基底クラスを用意します。メソッドが空実装なのは、このメソッドがAspectJにフックさせる為だけに存在しているからです。inter-type declarationを使うことも考えましたが、普通にコンパイルできる状態にしておきたいので。

public abstract class TestCaseBase extends TestCase{
    public void setMockAttribute(String key, Object value){}
}

モック環境を提供するAspect側の基底クラスを用意します。上記のメソッドを使ってモックに値を与える箇所をフックして、値をAspect側で保持するようにします。

@Aspect
public abstract class TestHelper {
    protected Map<String, Object> attributes = new HashMap<String, Object>();

    @Around("within(@UseMock *) && call(void *.setMockAttribute(..)) && args(key, value)")
    public void setMockAttribute(String key, Object value){
        attributes.put(key, value);
    }

    public Object getMockAttribute(String key){
        return attributes.get(key);
    }
}

上記のAspect側の基底クラスを継承して、Servlet環境を擬似的に実現する為のクラス(ServletTestHelper)を用意します。@UseMockアノテーションが付いているクラスのメソッドからの呼び出し時に限り、モック環境を提供します。@PointcutはAdviceのアノテーション(@Around等)内に記述することで省略することができますが、継承先で同じPointcutを使いたいケースもあると思うので別定義としました。ちょっと冗長ですが。

@Aspect
public class ServletTestHelper extends TestHelper {

    @Pointcut("cflow(within(@UseMock *)) && call(* javax.servlet.http.HttpServletRequest.getServerName())")
    public void getServerName(){};
    
    @Pointcut("cflow(within(@UseMock *)) && call(* javax.servlet.http.HttpServletRequest.getScheme())")
    public void getScheme(){};

    @Pointcut("cflow(within(@UseMock *)) && call(* javax.servlet.http.HttpServletRequest.getServerPort())")
    public void getServerPort(){};
    
    @Pointcut("cflow(within(@UseMock *)) && call(* javax.servlet.http.HttpServletRequest.getContextPath())")
    public void getContextPath(){};
    
    @Around("getServerName()")
    public String getDefaultServerName(){
        String serverName = (String)getMockAttribute("http.req.serverName");
        if(serverName != null){
            return serverName;
        }
        return "www.wrap-trap.net";        
    }
    @Around("getScheme()")
    public String getDefaultScheme(){
        String scheme = (String)getMockAttribute("http.req.scheme");
        if(scheme != null){
            return scheme;
        }
        return "http";        
    }
    @Around("getServerPort()")
    public int getDefaultServerPort(){
        Integer serverPort = (Integer)getMockAttribute("http.req.serverPort");
        if(serverPort != null){
            return serverPort;
        }
        return 8080;        
    }
    @Around("getContextPath()")
    public String getDefaultContentPath(){
        String contextPath = (String)getMockAttribute("http.req.contextPath");
        if(contextPath != null){
            return contextPath;
        }
        return "/foo";
    }    
}

最後にテストクラスを書きます。テストクラス内のsetMockAttributeを使い、モック環境から返される値を設定します。

@Aspect
@UseMock
public class RedirectForwarderTestCase extends TestCaseBase {
    
    private HttpServletRequest request;
        
    @Test
    public void testGetRedirectUrlHttp(){
        setMockAttribute("http.req.scheme", "http");
        setMockAttribute("http.req.serverPort", 80);
        RedirectForwarder redirectForwarder = new RedirectForwarder("/path");
        assertEquals("http://www.wrap-trap.net/foo/path", redirectForwarder.getRedirectUrl(request));
    }

    @Test
    public void testGetRedirectUrlHttp8080(){
        setMockAttribute("http.req.scheme", "http");
        setMockAttribute("http.req.serverPort", 8080);
        RedirectForwarder redirectForwarder = new RedirectForwarder("/bar");
        assertEquals("http://www.wrap-trap.net:8080/foo/bar", redirectForwarder.getRedirectUrl(request));
    }
    
    @Test
    public void testGetRedirectUrlSsl(){
        setMockAttribute("http.req.scheme", "https");
        setMockAttribute("http.req.serverPort", 443);
        RedirectForwarder redirectForwarder = new RedirectForwarder("/path");
        assertEquals("https://www.wrap-trap.net/foo/path", redirectForwarder.getRedirectUrl(request));
    }

    @Test
    public void testGetRedirectUrlSsl8443(){
        setMockAttribute("http.req.scheme", "https");
        setMockAttribute("http.req.serverPort", 8443);
        RedirectForwarder redirectForwarder = new RedirectForwarder("/path");
        assertEquals("https://www.wrap-trap.net:8443/foo/path", redirectForwarder.getRedirectUrl(request));
    }
}

HttpServletRequestのメソッドの呼び出しをフックしてAspectが起動します。callを使っている為、モック環境に設定された値を返す実装はメソッドの呼び出し側にweavingされます。ですのでHttpServletRequestのインスタンスは不要(=null)です。上記のテストクラスのように、HttpServletRequestをインスタンスフィールドにする必要も特にありませんが、テストメソッドの内部で宣言すると見た目が不思議な感じになるので、インスタンスフィールドに置いてます。