kagamihogeの日記

kagamihogeの日記です。

spring-boot + MockServer

spring-bootのweb-apiのintegration testでMockServerを使用する。

ソースコード

build.gradle

springとの連携用のmockserver-spring-test-listener-no-dependenciesの依存性を追加する。

plugins {
  id 'org.springframework.boot' version '2.6.6'
  id 'io.spring.dependency-management' version '1.0.11.RELEASE'
  id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
  compileOnly {
    extendsFrom annotationProcessor
  }
}

repositories {
  mavenCentral()
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  compileOnly 'org.projectlombok:lombok'
  developmentOnly 'org.springframework.boot:spring-boot-devtools'
  annotationProcessor 'org.projectlombok:lombok'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'

  testImplementation 'org.mock-server:mockserver-spring-test-listener-no-dependencies:5.13.2'
  
}

tasks.named('test') {
  useJUnitPlatform()
}

外部アクセスを伴う適当なrest-apiを作成する。

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@SpringBootApplication()
public class SampleWireMockApplication {
  @Value("${sample.remote.path}")
  String path;

  @RequestMapping("/sample/{key}")
  public String sample(@PathVariable String key) {
    RestTemplate template = new RestTemplate();

    String result = template.getForObject(path + "/sample-api/" + key, String.class);
    return result;
  }

  public static void main(String[] args) {
    SpringApplication.run(SampleWireMockApplication.class, args);
  }
}

プロパティファイルに外部アクセス先URLを持つ。

server.port=8081

sample.remote.path=http://example.com:12345

テストコードを作成する。

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Map;

import org.junit.jupiter.api.Test;
import org.mockserver.client.MockServerClient;
import org.mockserver.model.Header;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import org.mockserver.springtest.MockServerTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;

@SpringBootTest(properties = { "sample.remote.path=${server.url}" }, webEnvironment = WebEnvironment.RANDOM_PORT)
@MockServerTest("server.url=http://localhost:${mockServerPort}")
public class ApplicationTest {
  @LocalServerPort
  int serverPort;

  @Autowired
  TestRestTemplate template;

  MockServerClient mockServerClient;

  @Test
  void test() {
    mockServerClient
      .when(HttpRequest
          .request()
          .withMethod("GET")
          .withPath("/sample-api/key123"))
      .respond(HttpResponse
          .response()
          .withStatusCode(200)
          .withHeader(
              new Header("Content-Type", "application/json; charset=utf-8"))
          .withBody("""
                  {
                    "sample-id": "id12345",
                    "sample-value": "value"
                  }
                  """));

    Map<String, String> actual = template.getForObject("http://localhost:" + serverPort + "/sample/key123", Map.class);
    Map<String, String> expected = Map.of("sample-id", "id12345", "sample-value", "value");

    assertEquals(expected, actual);
  }
}
  • webEnvironment = WebEnvironment.RANDOM_PORTでランダムポートでこのapiが起動する。
  • @MockServerTestでspring連携が有効になりMockServerClientフィールドを宣言すれば使用可能になる。
  • @MockServerTestserver.url=http://localhost:${mockServerPort}でmock-serverのURLを指定できる。
  • @SpringBootTestpropertiesで外部アクセスURLのプロパティをmock-serverのURLで上書きする。
  • mockServerClient.when...でモックの定義をする。
  • TestRestTemplateRestTemplateでも構わない。これからはWebTestClientが主流になるかも?
  • assertEqualsはサンプルコードなので簡易にMapでの検証にした。実際にはrecordとかデータ保持クラスに詰め替えたりしてから検証、になると思われる。

関連

spring-cloud-starter-contract-stub-runner(WireMock)によるintegration test

spring-bootで外部アクセスを伴うweb-apiのintegration testを記述する際は何らかのmock serverを必要とする。ここではmock serverにWireMockを使用し、また、spring-bootとWireMockの連携にSpring Cloud Contractを使用する。WireMock関連の記述は6. Spring Cloud Contract WireMockのあたりにある。

ソースコード

build.gradle

Spring Initializr のadd dependencies...で「wiremock」と入力すると候補に出てくるContract Stub Runnerを追加して出来たものを使用する。

plugins {
    id 'org.springframework.boot' version '2.6.6'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

ext {
    set('springCloudVersion', "2021.0.1")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

tasks.named('test') {
    useJUnitPlatform()
}

適当な外部アクセスするrest apiを作成する。

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@SpringBootApplication()
public class SampleWireMockApplication {
  @Value("${sample.remote.path}")
  String path;

  @RequestMapping("/sample/{key}")
  public String sample(@PathVariable String key) {
    RestTemplate template = new RestTemplate();

    String result = template.getForObject(path + "/sample-api/" + key, String.class);
    return result;
  }

  public static void main(String[] args) {
    SpringApplication.run(SampleWireMockApplication.class, args);
  }
}

src/main/resources/application.properties に外部アクセス先のURLをプロパティとして持つ。

server.port=8081

sample.remote.path=http://example.com:12345

テストコードを記述する。

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Map;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;

@SpringBootTest(properties = {
    "sample.remote.path=http://localhost:${wiremock.server.port}" }, webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
public class ApplicationTest {
  @LocalServerPort
  int serverPort;

  @Autowired
  TestRestTemplate template;

  @Test
  void test() {
    Map<String, String> actual = template.getForObject("http://localhost:" + serverPort + "/sample/key123", Map.class);
    Map<String, String> expected = Map.of("sample-id", "id12345", "sample-value", "value");
    
    assertEquals(expected, actual);
  }
}
  • webEnvironment = WebEnvironment.RANDOM_PORTでこのspring-bootアプリケーションをランダムポートで起動する。
  • @LocalServerPortでそのランダムポート番号を取得できるので、これを使用してapiにアクセスする。
  • @AutoConfigureWireMock(port = 0)でWireMockのauto-configを有効化する。また、port = 0でmock-serverもランダムポートで起動する。
  • ${wiremock.server.port}でmock-serverのランダムポート番号を取得できるので、プロパティファイル内の外部アクセスURLをmock-serverのホスト(localhost)・ランダムポートで上書きする。
  • TestRestTemplateRestTemplateでも構わない。これからはWebTestClientが主流になるかも?
  • assertEqualsはサンプルコードなので簡易にMapでの検証にした。実際にはrecordとかデータ保持クラスに詰め替えたりしてから検証、になると思われる。

次にmock-serverが返す定義ファイルを作成する。デフォルトでは既定のディレクトリにマッピングファイルとレスポンス定義ファイルを作成する。ディレクトリの設定変更方法などは公式ドキュメントを参照。

まずマッピングファイルはsrc/test/resources/mappingsに作成する。以下はsrc/test/resources/mappings/sample-mapping.jsonとして作成した。これの中身はWireMockのドキュメントを参照。

{
  "request": {
    "method": "GET",
    "url": "/sample-api/key123"
  },
  "response": {
    "status": "200",
    "bodyFileName": "sample.json",
    "headers": {
      "Content-Type": "applicaion/json"
    }
  }
}

次にレスポンス定義ファイルはsrc/test/resources/__filesに作成する。以下はsrc/test/resources/__files/sample.jsonとして作成した。上記のマッピングファイルの"bodyFileName": "sample.json"がこのファイルを参照する形になる。

{
  "sample-id": "id12345",
  "sample-value": "value"
}

関連

javaのinitialCapacityの速度差

javaHashMapなどはコンストラクタでinitialCapacityで指定できる。単純ループでputしたとき速度差が生じるかを見る。

環境

やったこと

まず、ごく単純なループで固定文字列をputし続け、maxを100万と1000万と変えたときの速度を見てみる。

   static int max = 10_000_000;
    private static void hoge1() {
        long start = System.currentTimeMillis();
//     Map<Integer, String> map = new HashMap<>(max);
        Map<Integer, String> map = new HashMap<>();
        for (int i=0; i<max; i++) {
            map.put(i, "0123456789012345");
        }
        System.out.println(System.currentTimeMillis() - start);
    }

以下が測定結果で上が100万で下が1000万、ありがinitialCapacityを指定あり。

1 2 3
なし 101 97 105
あり 101 97 114
1 2 3
なし 700 706 709
あり 514 510 514

100万だと差は無いが1000万だと僅かに速くなっている。

次に、ループ内の処理を複雑にしてみる。具体的には以下のようにvalueをランダム文字列生成に変えてみる。

   private static void hoge2() {
         RandomStringGenerator generator = new RandomStringGenerator.Builder()
                 .build();
        long start = System.currentTimeMillis();
//     Map<Integer, String> map = new HashMap<>(max);
        Map<Integer, String> map = new HashMap<>();
        for (int i=0; i<max; i++) {
            map.put(i, generator.generate(16));
        }
        System.out.println(System.currentTimeMillis() - start);
    }
1 2 3
なし 33055 32992 33054
あり 33275 33040 32687

ほとんど差が無くなった。ループ内で重い処理があるとinitialCapacityで稼いだ分も誤差の範囲内に入ってしまうようだ。

感想とか

億単位まで件数を増やしてループ内の処理をあれこれ考慮しないと意味のある知見は得られなさそうである。ただ、少なくとも1000万くらいまでならinitialCapacityは指定無しで問題無さそう。初期値は優秀という事でもあろう。あとは、マシンスペックにも依存するのでやはり計測してみないことには何とも言え無さそうである。