kagamihogeの日記

kagamihogeの日記です。

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は指定無しで問題無さそう。初期値は優秀という事でもあろう。あとは、マシンスペックにも依存するのでやはり計測してみないことには何とも言え無さそうである。

docker(WSL2) + Testcontainers

Testcontainersユニットテストの起動と終了と共にdockerコンテナも起動と終了してくれるライブラリ。windowsというかWSL2で動かすには若干の追加設定が必要だった。

環境

手順

まず、Docker Desktopを使わずにWSL2でVSCode Remote Containerを使う - Qiitaにあるようにtcpをlistenする。

/etc/docker/daemon.json

{
  "debug": false,
  "tls": false,
  "hosts": ["tcp://127.0.0.1:2375", "unix:///var/run/docker.sock"]
}

次に、https://www.testcontainers.org/features/configuration/ にあるようにdocker.hostを修正する。C:/Users/myuser/.testcontainers.propertiesを以下のように修正する。

docker.host=tcp://localhost:2375

マニュアルによるとdocker.hostは以下三種類の指定方法があるとのことだが、自分の環境では3.のクラスパスは上手くいかなかった。なので2.のユーザホームに.testcontainers.properties(先頭に"."が必要なので注意)を置く方法にした。

1. Environment variables
2. .testcontainers.properties in user's home folder. Example locations:
  Linux: /home/myuser/.testcontainers.properties
  Windows: C:/Users/myuser/.testcontainers.properties
  macOS: /Users/myuser/.testcontainers.properties
3. testcontainers.properties on the classpath.