05. SpringBootのソースをスタブ化してみた


概要

SpringBootシリーズ第五弾です。
今回は今までと少し内容を変えて、ソースをスタブ化する方法について説明していきます。

よくあるのはUTにおけるMockitoだったり、FTにおけるwiremockによるモック化だと思うのですが、
bootRunしてしまうと各モックは動かないため、HTTP通信に失敗してしまったり、DBに繋げなかったりして困るということがあると思います。

今から紹介する方法でスタブ化させると、
ある環境ではスタブを使用し、対向APIが存在する環境だけは実際のAPIを叩きにいくということが可能になります。

困っている方の参考になればと思います!!

本題

ソースをスタブ化させるにあたってどんな方法をとるかというと、
スタブ化させたいクラスと同名のスタブクラスをDIコンテナに登録し、@Primaryを付与してすり替える
ということをやります。

1. 適当にRestTemplateを使って実装してみる

私の記事では何度か登場してきますが、郵便番号検索APIを利用します。
その名の通り、郵便番号をクエリパラメタに乗せてリクエストすると、レスポンスボディで住所情報を返却してくれるAPIです。

実装についてはこちらをご覧いただくとして、
どんな結果が返ってくるかbootRunして叩いてみましょう。

まずは存在する住所をリクエストしてみます。

無事、郵便番号や都道府県コードなどが取得できていますね。
正常系はこんな感じです。

では存在しない住所をリクエストするとどうなるでしょうか?

当然ですが、存在しない郵便番号を打てば空のレスポンスが返ってきます。

…それでは、もしも郵便番号検索APIが存在しない環境にこのアプリをリリースしなければならなくなったらどうしますか?
考えるまでもなくどんな郵便番号をリクエストしたとしても404エラーとなり、使い物にならなくなってしまいます。

そんな時のためにスタブを作って対応しましょう!!

2. スタブクラスを実装する

実装クラスをご覧いただきながら説明したほうがイメージしやすいと思いますので、
まずは実装クラスを載せます。

MockClient.java
@Configuration // (1)
public class MockClient {
    @Bean // (1)
    @Primary // (1)
    public GetAddressApiClient mockGetAddressApiClient(RestTemplateBuilder restTemplateBuilder,
                                                       ResponseHandlerInterceptor interceptor) {
        return new MockGetAddressApiClient(restTemplateBuilder, interceptor);
    }

    static class MockGetAddressApiClient extends GetAddressApiClient { // (2)
        MockGetAddressApiClient(RestTemplateBuilder restTemplateBuilder,
                                ResponseHandlerInterceptor interceptor) {
            super(restTemplateBuilder, interceptor);
        }

        @Override
        public GetAddressApiResponse request(String zipCode) {
            return new GetAddressApiResponse( // (3)
                    "200",
                    "",
                    Arrays.asList(
                            new GetAddressApiResponse.Result(
                                    "zipcode",
                                    "prefcode",
                                    "address1",
                                    "address2",
                                    "address3",
                                    "kana1",
                                    "kana2",
                                    "kana3"
                            ),
                            new GetAddressApiResponse.Result(
                                    "zipcode",
                                    "prefcode",
                                    "address1",
                                    "address2",
                                    "address3",
                                    "kana1",
                                    "kana2",
                                    "kana3"
                            )
                    )
            );
        }
    }
}

それぞれのポイントについて説明していきます!

(1). @Configuration+@Primary(@Bean)でDIコンテナに登録する

SpringではアノテーションによるBean定義が可能で、その際に使用する@Configuration+@BeanでスタブクラスをDIコンテナに登録します。
ただしこの時、本物のGetAddressApiClientも登録されているため、同名のBeanが二つ存在することになりSpringが例外を発生させます。
それを回避するために@Primaryを使用します!
これにより@Primaryが付与された方が優先されるようになります。
つまり常にスタブクラスが使用されるようになるわけですね。

(2). スタブ化対象のクラスを継承したスタブクラスを作る

続いてスタブクラス自体を作ります。
上記で書いたように今回は本物のクラスと同じ型でBean登録させる必要があります。
そのために本物のクラスを継承させてスタブクラスを作ります。割と大切です。
※コンストラクタはsuperを使って省略します。

(3). メソッドを@Overrideし、戻り値を定義する

あとは戻り値の定義です。
今回で言えばrequestメソッドをオーバーライドし、好きな戻り値を返すようにします。

試しに叩いてみる

bootRunして叩いてみましょう。

スタブに書いたレスポンスが返ってきていますので、
無事スタブ化できているようです!

…しかしここでまた一つ疑問が。
「このスタブが常に動いちゃうから、逆に本物のクラスが動かないのでは?」
…その通りです。今度は逆にスタブしか動かないアプリになってしまいました。

そこで環境ごとに本物とスタブを使い分けられるようにしてみましょう!!

3. スタブを適用させる環境を指定する

スタブクラスに@Profileを付与します
これによって指定された環境でだけスタブクラスがDI登録され、指定されていない環境ではスタブクラスがDI登録されない(≒本物が動く)ようになります。

MockClient.java
@Configuration
@Profile("dev")
public class MockClient {
    // 省略
}

上記の場合はdev環境でのみスタブクラスとなり、その他環境では本物のClientクラスが動くようになります!

最後に

最後までご覧いただきありがとうございました!
意外と調べてみるとこの手の記事が少ないように思いましたので、どなたかの役に立てれば幸いです!

ご指摘など、コメントお待ちしております!