Pocket

こんにちは。itouです。

今回は、外部サーバへHTTPリクエストする機能をJUnitでテストをする際
実際に外部サーバを建てることなくテストできる方法を記載いたします。

検証環境

  • Eclipse 4.4.1
  • Java 1.7
  • JUnit 4.12
  • JMockit 1.17
  • httpclient 4.4.1

Eclipse動作確認手順

Eclipseプロジェクトを添付しておりますので
動作を確認しながらご覧いただければ幸いです。
opentone_labs_201506.zip

  1. 「ファイル」-「インポート」-「既存プロジェクトをワークスペースへ」を選択
  2. 「アーカイブ・ファイルの選択」でopentone_labs_201506.zipを選択しインポート
  3. プロジェクトを右クリックし、「実行」-「JUnit テスト」
  4. JUnitが全て緑(成功)していることを確認

テスト対象機能説明

今回は「引数のURLに対してGETでHTTPリクエストを行い、HTTPステータスを返却する」という機能をテストします。
・HttpServiceImpl.java(テスト対象機能)

@Service
public class HttpServiceImpl implements HttpService {

    @Override
    public int requestUrl(String url) throws IOException {
        CloseableHttpClient httpClient = null;
        CloseableHttpResponse response = null;
        int statusCode;

        try {
            // CloseableHttpClientを生成
            httpClient = HttpClients.createDefault();

            // GETでHTTPリクエストを作成
            HttpGet request = new HttpGet(url);

            // HTTPリクエストを行い、HTTPレスポンスを取得
            response = httpClient.execute(request);

            // HTTPレスポンスよりHTTPステータスを取得
            statusCode = response.getStatusLine().getStatusCode();

        } finally {
            if (httpClient != null) {
                // CloseableHttpClientをclose
                httpClient.close();
            }

            if (response != null) {
                // CloseableHttpResponseをclose
                response.close();
            }
        }

        // HTTPステータスを返却
        return statusCode;

    }
}

外部サーバがある状態でのJUnit

比較の為、まずは外部サーバがある状態でJUnitを実施してみます。
外部サーバにはexample.comを指定します。
ブラウザで https://example.com/ と指定すれば表示されるとおり、実存するサーバです。
・UnusedMockedHttpServiceTest.java

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "file:src/main/webapp/WEB-INF/application-context.xml" })
public class UnusedMockedHttpServiceTest {

    @Autowired
    private HttpService httpService;

    /**
     * HTTPステータス=200 OK
     *
     * @throws Exception
     *             Exception
     */
    @Test
    public void example200() throws Exception {
        // http://example.com/ にHTTPリクエスト
        int actual = httpService.requestUrl("http://example.com/");

        // HTTPステータスが200 OKであること
        assertThat(actual, is(HttpStatus.OK.value()));
    }

    /**
     * HTTPステータス= 404 Not Found
     *
     * @throws Exception
     *             Exception
     */
    @Test
    public void example404() throws Exception {
        // http://example.com/not_found にHTTPリクエスト
        int actual = httpService.requestUrl("http://example.com/not_found");

        // HTTPステータスが404 Not Foundであること
        assertThat(actual, is(HttpStatus.NOT_FOUND.value()));
    }
}

http://example.com/ は存在するURLなので
HTTPステータス 200:OKが返却されます

http://example.com/not_found は存在しないURLなので
HTTPステータス 404 Not Foundが返却されます

外部サーバがない状態でのJUnit

いよいよ、本題の
外部サーバを使用せずにテストをした場合の方法を記載いたします。
外部サーバを指定してリクエストを発行する代わりに、
どのような記述になっているか着目してみてください。

・UsedMockedHttpServiceTest.java

    /**
     * HTTPステータス=500 Internal Server Error
     *
     * @throws Exception
     *             Exception
     */
    @Test
    public void example500() throws Exception {
        // HTTPステータス500 Internal Server Errorが返却されるようモックに設定
        mockHttpClientSet(HttpStatus.INTERNAL_SERVER_ERROR.value());

        // モックを使用するので、実際には ★dummy★ にリクエストされない
        int actual = httpService.requestUrl("★dummy★");

        // HTTPステータスが500 Internal Server Errorであること
        assertThat(actual, is(HttpStatus.INTERNAL_SERVER_ERROR.value()));
    }

requestUrlの引数に”★dummy★”と指定して、外部サーバを指定せずにリクエストを発行しましたが
HTTPステータス「500 Internal Server Error」が返却されてテストOKとなっています。

HTTPリクエスト前に実施している「mockHttpClientSet」にご注目ください。
後述する「モック」と呼ばれる仕組みを利用すると、返却するHTTPステータスをあらかじめ指定することができます。
これによって外部サーバに実際にアクセスしなくても、テストしたい振る舞いに合わせた
HTTPリクエストの結果を指定することができます。

モックの仕組み

今回は「JMockit」というモックフレームワークを使用しています。
「JMockit」の詳細は http://jmockit.org/ をご参考ください。
今回は、レスポンス値を指定するモックの機能に絞ってご説明いたします。

モックを定義しているスーパークラスのAbstractMockedTest.javaを確認します
CloseableHttpClientをモック化し
CloseableHttpClientのexecuteを使用する場合は
mockHttpClientSetで指定のHTTPステータスにより生成した、HttpResponseImplDummyが返却されるようにしています。
・AbstractMockedTest.java

public abstract class AbstractMockedTest {

    @Mocked
    private CloseableHttpClient mockHttpClient;

    /**
     * CloseableHttpClient#execute のモック設定
     *
     * @param httpStatus
     *            HTTPステータス
     * @throws IOException
     *             IOException
     */
    protected void mockHttpClientSet(final int httpStatus) throws IOException {
        new NonStrictExpectations() {
            {
                // モックの対象メソッドを指定
                mockHttpClient.execute((HttpUriRequest) any);

                // CloseableHttpClient#executeの戻り値を生成
                result = new HttpResponseImplDummy(httpStatus);
            }
        };
    }

}

・HttpResponseImplDummy.java

public class HttpResponseImplDummy implements CloseableHttpResponse {
    /**
     * ステータスライン
     */
    private StatusLine statusLine;

    /**
     * HTTPレスポンスのうち、ステータスライン(HTTPステータス)を定義
     *
     * @param httpStatus
     *            HTTPステータス
     */
    public HttpResponseImplDummy(int httpStatus) {
        setStatusLine(new StatusLineImpl(httpStatus));
    }

    /**
     * ステータスラインを実装
     *
     * @author opentone
     *
     */
    public class StatusLineImpl implements StatusLine {
        private final ProtocolVersion protocolVersion;
        private final int statusCode;
        private final String reasonPhrase;

        private StatusLineImpl(int statusCode) {
            this.protocolVersion = new ProtocolVersion("HTTP", 1, 1);
            this.statusCode = statusCode;
            this.reasonPhrase = "OK";
        }

        @Override
        public ProtocolVersion getProtocolVersion() {
            return protocolVersion;
        }

        @Override
        public int getStatusCode() {
            return statusCode;
        }

        @Override
        public String getReasonPhrase() {
            return reasonPhrase;
        }
    }

    @Override
    public StatusLine getStatusLine() {
        return this.statusLine;
    }

    @Override
    public void setStatusLine(StatusLine arg0) {
        this.statusLine = arg0;

    }
:
:
以下略
:
:

まとめ

モックを利用することで外部サーバを建てずにHTTPリクエストのテストをする方法を紹介しました。
また今回はご紹介しませんでしたが、
モックの呼ばれた回数や、モックに渡されたパラメータの検証も可能です。
詳しくは、 http://jmockit.org/ をご参考ください。

HTTPリクエストのテストにおけるモック活用のご紹介でした。