Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

HTTP/2でもモックサーバを活用しよう

こんにちは
maron8676です。
梅雨に入ってじめじめした日が続いていますね。

今回はwiremockを使ってHTTP/2のモックサーバを作成する方法を紹介します。
HTTP/2という比較的新しいプロトコルであっても、
モックサーバを利用してテストコードも書いていきたいですね。

背景

HTTP/2について

2015年にRFCで承認され、最近普及してきているHTTPプロトコルです。*1

HTTP/2には、HTTP/2 over TLS(h2)とHTTP/2 over TCP(h2c)という2つの方式があります。
h2ではサーバ証明書の設定など、HTTPバージョン以外の話も多くなってしまうため、
本記事ではh2cについて取り扱います*2

モックサーバ

本記事では、外部サービスや、他の内部サービスの処理を模倣するサーバを指します。
システム開発では、外部APIと連携したり、内部でサービスを分割して開発することがたびたびあります。

しかし、モックサーバがないと実施したいテストを実行できない問題が発生します。
例えば、図1のような処理をテストする場合を考えます。単純に実行してテストできればよいのですが、
外部APIが利用できない状況だと、図中の処理3といった後続処理で値が使用できずエラーになる状況が出てきます。

モックサーバを利用することで、外部サービスや他の内部サービスを直接利用できない状況でも、システム連携を想定したテストを行えるため、品質の高い状態で開発を進めることができます。

図1 外部APIを利用する処理

モックサーバのHTTP/2対応状況

モックサーバを作成するためのツールとしては以下が挙げられますが、
2021/06/05現在、HTTP/2に対応しているのはWiremockのみとなっています。
そのため、本記事ではWiremockを使ってモックサーバを作成したいと思います。*3

1. Wiremock
2. MockServer
3. Karate

HTTP/2のモックサーバ作成

では、Wiremockでモックサーバ作成を作成するコードを見ていきましょう。

1. Spring Initializrでプロジェクトを作成する
今回は、Maven Project、Packaging JarのJavaプロジェクトを作成します。

2. DemoApplication.javaを書き換える
以下のコードに書き換えます。
createHttpConnectorメソッドをオーバーライドして、h2cのコネクタを追加で指定してあげることによってh2cで通信できるサーバとなります。

3. ビルドしてjarファイルを作成する

mvn clean build

でモックサーバのjarファイルを作成できます。

Wiremock公式からダウンロードできるstandaloneのjarファイル(公式jarファイル)はh2c設定がされていないため、
自分でjarファイルを作成する必要があります。

4. モック動作を設定する
作成したjarは、公式jarファイルと同じようにmappingsディレクトリに配置されたjsonファイルを読み取ってモック動作を変更できます。
公式ドキュメントを見て、必要な動作を定義しましょう。

package com.example.demo;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.common.ConsoleNotifier;
import com.github.tomakehurst.wiremock.common.JettySettings;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.github.tomakehurst.wiremock.jetty94.Jetty94HttpServer;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.io.NetworkTrafficListener;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.ServerConnector;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.List;

import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;

@SpringBootApplication
public class DemoApplication implements ApplicationRunner {
    public static void main(String[] args) {
        SpringApplication.run(WiremockHttp2Application.class, args);
    }

    @Override
    public void run(ApplicationArguments args) {
        boolean hasPort = args.containsOption("port");
        List<String> ports = args.getOptionValues("port");
        boolean hasDirectory = args.containsOption("rootDir");
        List<String> dirs = args.getOptionValues("rootDir");
        if (!hasPort || ports.size() == 0 || StringUtils.isBlank(ports.get(0))) {
            System.out.println("--portでポート番号を指定してください。");
            return;
        }
        if (!hasDirectory || dirs.size() == 0 || StringUtils.isBlank(dirs.get(0))) {
            System.out.println("--rootDirでルートディレクトリを指定してください。");
            return;
        }

        int port = Integer.parseInt(ports.get(0));
        WireMockConfiguration configuration = wireMockConfig().port(port);
        configuration.withRootDirectory(dirs.get(0));
        configuration = configuration.notifier(new ConsoleNotifier(true));

        configuration = configuration.httpServerFactory((options, adminRequestHandler, stubRequestHandler) ->
                new Jetty94HttpServer(options, adminRequestHandler, stubRequestHandler) {
                    @Override
                    protected ServerConnector createHttpConnector(
                            String bindAddress,
                            int port,
                            JettySettings jettySettings,
                            NetworkTrafficListener listener) {
                        HttpConfiguration httpConfig = createHttpConfig(jettySettings);
                        HttpConnectionFactory http11 = new HttpConnectionFactory(
                                httpConfig);
                        HTTP2CServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(
                                httpConfig);

                        ServerConnector connector = createServerConnector(bindAddress,
                                jettySettings, port, listener, http11, h2c);

                        return connector;
                    }
                });
        WireMockServer server = new WireMockServer(configuration);
        server.start();
    }
}

動作確認

これで、図1における外部APIを含めたテストをするためのモックサーバを作成できました。
サーバにリクエストをするプログラムの代わりに、curlコマンドでリクエストを送って動作確認してみましょう。

今回はサンプルとして、以下のモック定義を準備しています。
これは公式ドキュメントで紹介されている定義です。
/some/thingにGETリクエストがあった際に、text/plainでHello world!とレスポンスを返します。

{
    "request": {
        "method": "GET",
        "url": "/some/thing"
    },
    "response": {
        "status": 200,
        "body": "Hello world!",
        "headers": {
            "Content-Type": "text/plain"
        }
    }
}

curlコマンドでは --http2 を付けることでHTTP/2でのリクエストとなります。

$ curl -v --http2 localhost:8080/some/thing
*   Trying 127.0.0.1:8080...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
----------1----------
> GET /some/thing HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
>
* Mark bundle as not supporting multiuse
----------2----------
< HTTP/1.1 101 Switching Protocols
* Received 101
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
----------3----------
< HTTP/2 200
< content-type: text/plain
< matched-stub-id: 6fc1a757-7c85-4315-8f4f-cc52b41aadfa
< vary: Accept-Encoding, User-Agent
<
* Connection #0 to host localhost left intact
Hello world!

実行時ログの説明

各部分で何が起こっているのか見ていきましょう。

1. クライアント側からは、Upgradeヘッダにh2cを設定することで、
プロトコルをh2cにアップグレードできないかサーバに問い合わせています。

2. サーバはHTTP/1.1 101 Switching Protocolsを返すことで、
HTTP/2へのアップグレードを行ったことをクライアント側に通知しています。

3. HTTP/2 200 の表示からHTTP/2で通信していることを確認できます。
HTTP/2から、ヘッダのフィールド名が小文字で統一されましたが、
コンソール出力からもcontent-typeのフィールド名が小文字になっていることを確認できます。

---1---などは説明のための目印として挿入したもので、実際に出力されたログではありません。

まとめ

HTTP/2の一方式であるh2cに対応したモックサーバの作成方法を紹介しました。
プロダクトコードと並行してテストコードも実装し、品質を上げていけるとよいですね。
また、未検証のため記載していませんが、Wiremockはh2にも対応しているようなので試してみようと思います。

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

エンジニア募集!100%オンライン、好きな場所で働けます! - Acroquest Technology株式会社のエンジニアリングの求人 - Wantedlywww.wantedly.com

*1:https://www.rfc-editor.org/rfc/rfc7540.txt

*2:主要ブラウザはh2しか対応していないのですが、システム内の内部通信など、h2cを利用する状況もあると考えています

*3:MockServerとKarateでは、HTTP/2対応は提案段階となっています。
MockServer https://trello.com/b/dsfTCP46/mockserver
Karate https://github.com/intuit/karate/projects/3