単体テストで ImagesService#applyTransform を使う方法

slim3ではGAEの環境に依存するような単体テストもかなり簡単に行う事ができます。例えば、Bigtableへのアクセスはテストの実行毎にリセットされる、グローバルトランザクションに対応など至れりつくせりです。

ところが、画像を加工してサムネイルを作る時などに使用する ImagesServiceですが、これを単体テストで実行しようとすると例外が発生します。内部的には、画像の作成や一部の軽い処理はImplで行っているようですが、拡大縮小などの変換処理は各アプリでやるのではなく、外部サービスとして呼び出しているようです。これらのテスト環境でのエミュレートは、org.slim3.tester.AppEngineTesterのmakeSyncCallで行っていますので、ここを少しカスタマイズすることでImagesServiceもエミュレートできるようになります。

以下、簡単なサンプルになります。

public class MyTester extends org.slim3.tester.ControllerTester {
    private static final String IMAGE_SERVICE = "images";
    public final ImageServiceStub imageServiceStub = new ImageServiceStub();

    @Override
    public byte[] makeSyncCall(Environment env, String service, String method, byte[] requestBuf)
            throws ApiProxyException {
        if (service.equals(IMAGE_SERVICE)) {
            Queue<byte[]> queue = imageServiceStub.imageStore.get(method);
            if (queue == null || queue.isEmpty()) throw new ApiProxyException("service=" + service + ", method=" + method);
            ImagesServicePb.ImagesTransformResponse res =
                    new ImagesServicePb.ImagesTransformResponse();
            ImagesServicePb.ImageData data = new ImagesServicePb.ImageData();
            data.setContentAsBytes(queue.peek());
            res.setImage(data);
            return res.toByteArray();
        }
        return super.makeSyncCall(env, service, method, requestBuf);
    }
    
    public class ImageServiceStub {
        Map<String, Queue<byte[]>> imageStore = new HashMap<String, Queue<byte[]>>();
        public void register(String method, byte[] imageData) {
            Queue<byte[]> queue = imageStore.get(method);
            if (queue == null) {
                queue = new LinkedList<byte[]>();
                imageStore.put(method, queue);
            }
            queue.add(imageData);
        }
    }
}

makeSyncCallはサービス名と実行メソッド名を引数に呼び出されます。ImageServiceの場合imagesなので、それをヒントに処理を上書きしましょう。requestBufは引数の情報などが格納されているので、必要な場合、適切なオブジェクトに戻します(ここではImagesServicePb.ImagesTransformRequest)。同様に戻り値もbyteに変換しますが、同様にImagesServicePb.ImagesTransformResponseを作成し、toByteArrayでbyteに戻してから返します。
後は変換処理のイメージについて、適当に登録できるようにし、メソッドの呼び出し順に返すようにしただけです。

実際にテストする場合はこんな感じ。

    @Test
    public void upload_post() throws Exception {
        // ImageServiceで返却される画像を登録
        tester.imageServiceStub.register("Transform", BinLoader.load("thumbnail.jpg"));
        FileItem item = new FileItem("sample.jpg", "image/jpg", BinLoader.load("sample.jpg"));
        tester.request.setMethod("post");
        tester.request.setAttribute("file", item);
        tester.start("/_mng/media/upload");
        assertThat(tester.response.getStatus(), is(HttpServletResponse.SC_MOVED_TEMPORARILY));
        assertThat(tester.isRedirect(), is(true));
        assertThat(tester.getDestinationPath(), is("/_mng/media/"));
    }

この辺は、 @shin1ogawa さんのエントリーが非常に参考になりますので、あわせて参照ください。