kagamihogeの日記

kagamihogeの日記です。

WebTestClientによるintegration testの記述

spring-bootで作成するweb-apiのintegration test(以下IT)を記述するのにWebTestClientが使える。これは内部的にWebClientを使用する。同様にTestRestTemplateは内部的にRestTemplateを使用する。環境に応じて使い分けになる。今回はWebTestClientによるITの記述について。

ドキュメントは WebTestClient :: Spring Docsにあり、ここ読めばだいたいは困らないと思う。

ソースコード

build.gradle

plugins {
  id 'org.springframework.boot' version '2.6.7'
  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()
}

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.boot:spring-boot-starter-webflux'
}

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

注意点として、WebTestClientのinterfaceはspring-testにいるがWebClientの実クラスがないと実行時エラーになる。ので、依存性にwebfluxが必要になる。webfluxの依存性はtestImplementationにしているがこれは従来のwebでのITを想定してるため。webfluxを使うならtestにする必要はない。

controller

まず適当なcontrollerを作る。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication()
public class SampleWebTestClientApplication {
  public record SampleResponse(String first, String second, List<Integer> list) {
  }

  @RequestMapping("/sample")
  public SampleResponse sample() {
    return new SampleResponse("f", "s", List.of(1, 2, 3));
  }

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

test

テストクラスを作る。

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.web.server.LocalServerPort;
import org.springframework.test.web.reactive.server.WebTestClient;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationTest {
  @LocalServerPort
  int port;
  
  @Autowired
  WebTestClient client;
  
  @Test
  void test() {
    String json = """
        {
          "second":"s",
          "first":"f"  ,
          
          "list":[3,2,1]
        }
        """;
    client.get().uri("/sample")
      .exchange()
      .expectStatus().isOk()
      .expectBody().json(json);
    }
}
  • @SpringBootTestは今まで通りのapiを実際に起動してのテストを行うためのアノテーション
  • WebTestClient@SpringBootTestによって自動的にbeanが作られる。どうゆうbeanが作られるかはここを参照。色々設定してなければ、 WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build()とおおむね同様と見てよい。
  • apiにアクセスしてレスポンスのjson文字列同士を比較している。javaのクラスにdeserializeしてから比較しても良いと思う。ただ、デフォルトで割と柔軟なjsonチェックをしてくれるので、ITとしてはこれで良いんじゃないかな、とか思う。
  • 柔軟なチェックの概要はソースコードにもある通り、空白や改行・要素順序・配列の要素順序、を無視してくれる。また、expect側に要素が欠けててもokになる(ex {"second":"s"}でもok)。strictなチェックをしたい場合は.expectBody().json(json, true)にする*1
  • WebTestClientに色々あるassertでは不足していてカスタマイズしたい場合はconsumeWithを使う。以下はjson(expect)とほぼ同等の挙動になる。
    client.get().uri("/sample")
      .exchange()
      .expectStatus().isOk()
      .expectBody(String.class)
      .consumeWith(result -> {
        try {
          new JsonExpectationsHelper().assertJsonEqual(json, result.getResponseBody(), false);
        } catch (Exception e) {
          e.printStackTrace();
        }
      });

*1:5.3.16以降で指定可能