CLOVER🍀

That was when it all began.

REST AssuredをSpring Boot 3.0で使う

これは、なにをしたくて書いたもの?

REST Assuredが5.1.0でSpring Boot 3.0に対応したようなので、REST AssuredとSpring Frameworkの組み合わせの確認を含めて
試してみることにしました。

https://github.com/rest-assured/rest-assured/blob/rest-assured-5.3.0/changelog.txt#L90

REST AssuredとSpring Framework

REST AssuredをSpring Frameworkを組み合わせる方法は、MockMvcとWebTestClientの2種類から選ぶことができます。

それぞれ利用するモジュールが異なり、spring-mock-mvcspring-web-test-clientになります。

ドキュメントは、こちらにあります。

Spring · rest-assured/rest-assured Wiki · GitHub

ここで、1度Spring Frameworkのドキュメントを見てみます。

MockMvcにはセットアップ方法がスタンドアロンとSpring Configurationの2種類があります。

Testing / MockMvc / Setup Choices

一方で、REST Assuredのドキュメントにはスタンドアロンでの使用を想定した記載になっています。

WebTestClientの方は、モックサーバーに接続するか、起動しているサーバーに接続しているかを選ぶことができるようです。

Testing / WebTestClient / Setup

REST Assuredのドキュメントは、REST Assured側でWebTestClientを組み上げるAPIの利用方法が記載されています。

Spring Bootを使った場合は、MockMvcはSpring Configuration、WebTestClientもセットアップ済みなので、REST Assuredで使う時もそちらに
習おうと思うのですが、この場合ドキュメントの記載内容とは違ったAPIを使うことになりそうです。

この前提で試してみましょう。

お題は、以下で1度MockMvcとWebTestClientを使ったサンプルを書いているので、こちらを使うことにします。

Spring Web MVCアプリケーションのテスト方法を見ていきたい(+モックでBeanのテスト) - CLOVER🍀

ちょっとしたechoプログラムですが、こちらのテストをREST Assuredを使って書き直します。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.5 2022-10-18
OpenJDK Runtime Environment (build 17.0.5+8-Ubuntu-2ubuntu120.04)
OpenJDK 64-Bit Server VM (build 17.0.5+8-Ubuntu-2ubuntu120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.7 (b89d5959fcde851dcb1c8946a785a163f14e1e29)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.5, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-135-generic", arch: "amd64", family: "unix"

Spring Bootアプリケーションを作成する

それでは、Spring Bootアプリケーションを作成します。前述のとおり前に書いた以前のエントリーで書いたプログラムを利用しますが、
Spring Bootは3.0を使います。

Spring Initializrでプロジェクトを作成。依存関係にはwebwebfluxを指定します。

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=3.0.1 \
  -d javaVersion=17 \
  -d type=maven-project \
  -d name=spring-boot3-rest-assured-example \
  -d groupId=org.littlewings \
  -d artifactId=spring-boot3-rest-assured-example \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=org.littlewings.spring.restassured \
  -d dependencies=web,webflux \
  -d baseDir=spring-boot3-rest-assured-example | tar zxvf -

webfluxは、WebTestClientの利用に必要です。MockMvcのみの場合は、REST Assuredを使う場合も含めてwebfluxは必要ありません。

プロジェクト内に移動。

$ cd spring-boot3-rest-assured-example

Mavenでの依存関係など。

        <properties>
                <java.version>17</java.version>
        </properties>
        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-webflux</artifactId>
                </dependency>

                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
                <dependency>
                        <groupId>io.projectreactor</groupId>
                        <artifactId>reactor-test</artifactId>
                        <scope>test</scope>
                </dependency>
        </dependencies>

        <build>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>

デフォルトで作成されているソースコードは削除しておきます。

$ rm src/main/java/org/littlewings/spring/restassured/SpringBoot3RestAssuredExampleApplication.java src/test/java/org/littlewings/spring/restassured/SpringBoot3RestAssuredExampleApplicationTests.java

では、ソースコードを作成します。

RestController。GETでもPOSTでもメッセージを受け付けて、レスポンスに含めて返すことにします。

src/main/java/org/littlewings/spring/restassured/EchoController.java

package org.littlewings.spring.restassured;

import java.util.Optional;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("echo")
public class EchoController {
    MessageDecorationService messageDecorationService;

    public EchoController(MessageDecorationService messageDecorationService) {
        this.messageDecorationService = messageDecorationService;
    }

    @GetMapping("get")
    public EchoResponse get(@RequestParam String message) {
        // Thread.dumpStack();

        return EchoResponse
                .create(
                        messageDecorationService
                                .decorate(Optional.ofNullable(message).orElse("Hello World[get]!!")),
                        Thread.currentThread().getName()
                );
    }

    @PostMapping("post")
    public EchoResponse post(@RequestBody EchoRequest echoRequest) {
        // Thread.dumpStack();

        return EchoResponse
                .create(
                        messageDecorationService
                                .decorate(Optional.ofNullable(echoRequest.getMessage()).orElse("Hello World[post]!!")),
                        Thread.currentThread().getName()
                );
    }
}

レスポンス内には、現在のスレッド名を含めます。

リクエストで使うクラス。

src/main/java/org/littlewings/spring/restassured/EchoRequest.java

package org.littlewings.spring.restassured;

public class EchoRequest {
    String message;

    public static EchoRequest create(String message) {
        EchoRequest echoRequest = new EchoRequest();
        echoRequest.setMessage(message);

        return echoRequest;
    }

    // getter/setterは省略
}

レスポンスで使うクラス。

src/main/java/org/littlewings/spring/restassured/EchoResponse.java

package org.littlewings.spring.restassured;

public class EchoResponse {
    String message;
    String threadName;

    public static EchoResponse create(String message, String threadName) {
        EchoResponse echoResponse = new EchoResponse();
        echoResponse.setMessage(message);
        echoResponse.setThreadName(threadName);

        return echoResponse;
    }

    // getter/setterは省略
}

RestControllerが使うService。リクエストで受け取ったメッセージを少し装飾して返すことにします。

src/main/java/org/littlewings/spring/restassured/MessageDecorationService.java

package org.littlewings.spring.restassured;

import org.springframework.stereotype.Service;

@Service
public class MessageDecorationService {
    public String decorate(String message) {
        return String.format("★★★ %s !!★★★", message);
    }
}

mainメソッドを持ったクラス。

src/main/java/org/littlewings/spring/restassured/App.java

package org.littlewings.spring.restassured;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

ここまでで、アプリケーションの準備は完了です。

動作確認しておきましょう。

$ mvn spring-boot:run

こんな感じになります。

## GET

$ curl localhost:8080/echo/get?message=Hello
{"message":"★★★ Hello !!★★★","threadName":"http-nio-8080-exec-1"}


## POST
$ curl -H 'Content-Type: application/json' localhost:8080/echo/post -d '{"message": "Hello"}'
{"message":"★★★ Hello !!★★★","threadName":"http-nio-8080-exec-2"}

REST Assuredを使ってテストをする

それでは、このサンプルアプリケーションに対してREST Assuredを使ってテストを書いていきます。

REST Assured+MockMvc

まずは、REST AssuredとMockMvcを組み合わせます。

Maven依存関係に、spring-mock-mvcを追加。

     <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>spring-mock-mvc</artifactId>
            <version>5.3.0</version>
            <scope>test</scope>
        </dependency>

spring-mock-mvcに関する記述は、こちら。

Spring Support / Spring Mock Mvc Module

作成したテストコードは、こちら。

src/test/java/org/littlewings/spring/restassured/MockMvcWithRestAssuredTest.java

package org.littlewings.spring.restassured;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static io.restassured.module.mockmvc.RestAssuredMockMvc.given;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
public class MockMvcWithRestAssuredTest {
    @Autowired
    MockMvc mvc;

    @Test
    public void getTest() {
        given()
                .queryParam("message", "MockMvc[get]")
                .mockMvc(mvc)  // MockMvc
                .when()
                .get("/echo/get")
                .then()
                .assertThat(status().isOk())  // MockMvcResultMatchers
                .body("message", is("★★★ MockMvc[get] !!★★★"))
                .body("threadName", is("main"));
    }

    @Test
    public void postTest() {
        given()
                .contentType(MediaType.APPLICATION_JSON)
                .body(EchoRequest.create("MockMvc[post]"))
                .mockMvc(mvc)  // MockMvc
                .when()
                .post("/echo/post")
                .then()
                .assertThat(status().isOk())  // MockMvcResultMatchers
                .header("content-type", MediaType.APPLICATION_JSON_VALUE)
                .body("message", is("★★★ MockMvc[post] !!★★★"))
                .body("threadName", is("main"));
    }
}

ポイントをいくつか。

まず、MockMvcを使うので@AutoConfigureMockMvcアノテーションを付与します。

@SpringBootTest
@AutoConfigureMockMvc
public class MockMvcWithRestAssuredTest {

テストコードの方は、パット見は通常のREST Assuredを使ったコードです。MockMvcは直接操作しません。

    @Test
    public void getTest() {
        given()
                .queryParam("message", "MockMvc[get]")
                .mockMvc(mvc)  // MockMvc
                .when()
                .get("/echo/get")
                .then()
                .assertThat(status().isOk())
                .body("message", is("★★★ MockMvc[get] !!★★★"))
                .body("threadName", is("main"));
    }

import文を見ると、使っているのはRestAssuredMockMvcになっています。

import static io.restassured.module.mockmvc.RestAssuredMockMvc.given;

また、MockMvcRequestSpecification#mockMvcMockMvcインスタンスを設定することも必要です。

        given()
                .queryParam("message", "MockMvc[get]")
                .mockMvc(mvc)  // MockMvc

REST Assuredのドキュメントの場合、この部分がスタンドアロンでの利用想定になっています。

Spring Support / Spring Mock Mvc Module / Bootstrapping RestAssuredMockMvc

具体的には、MockMvcRequestSpecification#standaloneSetupを使うように書かれています。

given().standaloneSetup(new GreetingController()). ..

今回はSpring Bootで設定済みのMockMvcを使うため、MockMvcRequestSpecification#mockMvcMockMvcインスタンスを指定することに
なります。

また、ValidatableMockMvcResponse#assertThatMockMvcResultMatchersのメソッドも使うことができます。

Spring Support / Spring Mock Mvc Module / Using Result Matchers

RestControllerを動かしているスレッドはmainスレッドなので、モック環境でテストを行っていることも確認できます。

                .body("threadName", is("main"));

REST AssuredとMockMvcの組み合わせは、とりあえずこんなところでしょうか。

もう少し詳しい使い方は、ドキュメントを参照。

Spring Support / Spring Mock Mvc Module

REST Assured+WebTestClient

次は、REST AssuredとWebTestClientを組み合わせます。

Maven依存関係に、spring-web-test-clientを追加。

     <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>spring-web-test-client</artifactId>
            <version>5.3.0</version>
            <scope>test</scope>
        </dependency>

spring-web-test-clientに関するドキュメントは、こちらです。

Spring Support / Spring Web Test Client Module

ドキュメントはリアクティブなControllerを対象にして書かれていますが、ふつうのSpring Web MVCで書かれたControllerでも構いません。
WebTestClientがそういうものですからね。

作成したテストコードは、こちら。

src/test/java/org/littlewings/spring/restassured/WebTestClientWithRestAssuredTest.java

package org.littlewings.spring.restassured;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;

import static io.restassured.module.webtestclient.RestAssuredWebTestClient.given;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebTestClientWithRestAssuredTest {
    @Autowired
    WebTestClient webClient;

    @Test
    public void getTest() {
        given()
                .queryParam("message", "WebEnvironment Test[get]")
                .webTestClient(webClient)  // WebTestClient
                .when()
                .get("/echo/get")
                .then()
                .statusCode(200)
                .body("message", is("★★★ WebEnvironment Test[get] !!★★★"))
                .body("threadName", startsWith("http-nio-auto-1-exec-"));
    }

    @Test
    public void postTest() {
        given()
                .contentType(MediaType.APPLICATION_JSON)
                .body(EchoRequest.create("WebEnvironment Test[post]"))
                .webTestClient(webClient)  // WebTestClient
                .when()
                .post("/echo/post")
                .then()
                .statusCode(200)
                .header("content-type", MediaType.APPLICATION_JSON_VALUE)
                .body("message", is("★★★ WebEnvironment Test[post] !!★★★"))
                .body("threadName", startsWith("http-nio-auto-1-exec-"));
    }
}

こちらもポイントをいくつか。

テストは組み込みサーバーを使って行うため、SpringBootTestwebEnvironmentRANDOM_PORTまたはDEFINED_PORTを指定します。
今回はRANDOM_PORTを指定しました。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebTestClientWithRestAssuredTest {

こちらもパット見はふつうのREST Assuredを使ったテストに見えます。WebTestClient自体は直接操作しません。

    @Test
    public void getTest() {
        given()
                .queryParam("message", "WebEnvironment Test[get]")
                .webTestClient(webClient)  // WebTestClient
                .when()
                .get("/echo/get")
                .then()
                .statusCode(200)
                .body("message", is("★★★ WebEnvironment Test[get] !!★★★"))
                .body("threadName", startsWith("http-nio-auto-1-exec-"));
    }

ここでimport文を見ると、使っているのはRestAssuredWebTestClientになっています。

import static io.restassured.module.webtestclient.RestAssuredWebTestClient.given;

また、WebTestClientRequestSpecification#webTestClientWebTestClientインスタンスを設定することも必要です。

        given()
                .queryParam("message", "WebEnvironment Test[get]")
                .webTestClient(webClient)  // WebTestClient

MockMvcの時と同様に、WebTestClient向けのモジュールの場合もこの部分がスタンドアロン向けのドキュメントの記載になっています。

Spring Support / Spring Web Test Client Module / Bootstrapping RestAssuredWebTestClient

こちらもWebTestClientRequestSpecification#standaloneSetupを使うように書かれています。

given().standaloneSetup(new GreetingController()). ..

今回はSpring Bootで設定済みのWebTestClientを使うため、WebTestClientRequestSpecification#webTestClientWebTestClient
インスタンスを指定することになります。

その他は、通常のREST Assuredを使った書き方と変わりません。

レスポンスに含まれているスレッド名もTomcatのものになっているので、組み込みTomcatを使って動作していることがわかります。

                .body("threadName", startsWith("http-nio-auto-1-exec-"));

REST AssuredとWebTestClientの組み合わせも、こんなところでしょう。

ドキュメントの記述量は、MockMvc向けのモジュールと比べて多くありません。

Spring Support / Spring Web Test Client Module

まとめ

REST AssuredがSpring Boot 3.0に対応したようなので、REST AssuredのSpring向けのモジュールと合わせて試してみました。

MockMvcWebTestClientの存在をあまり意識せず、REST Assuredの書き方で書けるのが良いですね。