💯

RestClient + WireMockで一部のテストだけ失敗する現象+対策

に公開

真の原因は分かっていなくて、とりあえずこうしたら動いたというレベルの記事です。

環境

  • JDK 25
  • Spring Boot 4.0.1
  • WireMock 3.13.2
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.example</groupId>
    <artifactId>rest-client-sample</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>25</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-restclient</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-restclient-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.wiremock</groupId>
            <artifactId>wiremock-standalone</artifactId>
            <version>3.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

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

</project>

テスト対象

@Component
public class TodoClient {
    private final RestClient restClient;

    public TodoClient(
            RestClient.Builder restClientBuilder,
            @Value("${todo-service.base-url}") String baseUrl
    ) {
        this.restClient = restClientBuilder
                .baseUrl(baseUrl)
                .build();
    }

    // GET・POST・PUT・DELETE・PATCHなどのメソッドが色々ある
}
application.properties
spring.application.name=rest-client-sample

# 接続タイムアウト
spring.http.clients.connect-timeout=1s
# 読み取りタイムアウト
spring.http.clients.read-timeout=1s
# JdkClientHttpRequestFactoryを使う
spring.http.clients.imperative.factory=jdk

todo-service.base-url=http://localhost:9999

テストコード

@WireMockTest(httpPort = 9999)
@SpringBootTest
public class TodoClientTest {
    @Autowired
    TodoClient todoClient;

    @Nested
    @DisplayName("getById()")
    class GetByIdTest {
        @Test
        @DisplayName("IDを指定すると、該当するTODOのOptionalを取得できる")
        void success() {
            // WireMockの設定
            stubFor(get("/api/todos/1")
                    .willReturn(okJson("""
                            {
                                "id": 1,
                                "description": "Example 1",
                                "completed": true,
                                "deadline": "2025-10-01T12:00:00",
                                "createdAt": "2025-09-01T12:00:00"
                            }
                            """)));
            // テストの実行
            Optional<TodoResponse> actual = todoClient.getById(1);
            // 結果の検証
            assertEquals(
                    new TodoResponse(
                            1,
                            "Example 1",
                            true,
                            LocalDateTime.parse("2025-10-01T12:00:00"),
                            LocalDateTime.parse("2025-09-01T12:00:00")
                    ), actual.get()
            );
        }

    // 他にもテストがたくさん

テストを実行すると

なぜか一部のテストだけ成功して、その他のほとんどがNGになる。

エラーメッセージはこんな感じ。

org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://localhost:9999/api/todos": null

	at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.createResourceAccessException(DefaultRestClient.java:763)
	at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.exchangeInternal(DefaultRestClient.java:615)
	at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.exchange(DefaultRestClient.java:567)
	at org.springframework.web.client.RestClient$RequestHeadersSpec.exchange(RestClient.java:750)
	at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.executeAndExtract(DefaultRestClient.java:896)
	at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.toBodilessEntity(DefaultRestClient.java:860)
	at com.example.restclientsample.TodoClient.register(TodoClient.java:55)
	at com.example.restclientsample.TodoClientTest$RegisterTest.success(TodoClientTest.java:165)
Caused by: java.net.ConnectException
	at java.net.http/jdk.internal.net.http.common.Utils.toConnectException(Utils.java:1070)
	at java.net.http/jdk.internal.net.http.PlainHttpConnection.connectAsync(PlainHttpConnection.java:223)
	at ...
Caused by: java.nio.channels.ClosedChannelException
	at java.base/sun.nio.ch.SocketChannelImpl.ensureOpen(SocketChannelImpl.java:204)
	at java.base/sun.nio.ch.SocketChannelImpl.connect(SocketChannelImpl.java:942)
	at java.net.http/jdk.internal.net.http.PlainHttpConnection.connectAsync(PlainHttpConnection.java:211)
	... 139 more

対策1: @RegisterExtensionを使う

@WireMockTestではなく、@RegisterExtensionを使ってみた。

@SpringBootTest
public class TodoClientTest {
    @Autowired
    TodoClient todoClient;

    // コレ!!!
    @RegisterExtension
    static WireMockExtension wireMock = WireMockExtension.newInstance()
            .options(wireMockConfig()
                    .port(9999))
            .build();

    @Nested
    @DisplayName("getById()")
    class GetByIdTest {
        @Test
        @DisplayName("IDを指定すると、該当するTODOのOptionalを取得できる")
        void success() {
            // ここも少し変更!!!
            wireMock.stubFor(get("/api/todos/1")
                ...
        }

    // 他にもテストがたくさん

これでテストを実行すると、OKになるテストは増えたけど、まだNGなテストが残っている。

エラーメッセージはこんな感じ。


org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://localhost:9999/api/todos": Received RST_STREAM: Stream cancelled

	at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.createResourceAccessException(DefaultRestClient.java:763)
	at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.exchangeInternal(DefaultRestClient.java:615)
	at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.exchange(DefaultRestClient.java:567)
	at org.springframework.web.client.RestClient$RequestHeadersSpec.exchange(RestClient.java:750)
	at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.executeAndExtract(DefaultRestClient.java:896)
	at org.springframework.web.client.DefaultRestClient$DefaultResponseSpec.toBodilessEntity(DefaultRestClient.java:860)
	at com.example.restclientsample.TodoClient.register(TodoClient.java:55)
	at com.example.restclientsample.TodoClientTest$RegisterTest.success(TodoClientTest.java:163)
Caused by: java.io.IOException: Received RST_STREAM: Stream cancelled
	at java.net.http/jdk.internal.net.http.Stream.handleReset(Stream.java:778)
	at java.net.http/jdk.internal.net.http.Stream.incoming_reset(Stream.java:709)
	at java.net.http/jdk.internal.net.http.Stream.otherFrame(Stream.java:588)
	at java.net.http/jdk.internal.net.http.Stream.incoming(Stream.java:581)
	at java.net.http/jdk.internal.net.http.Http2Connection.processFrame(Http2Connection.java:1072)
	at java.net.http/jdk.internal.net.http.frame.FramesDecoder.decode(FramesDecoder.java:155)
	at java.net.http/jdk.internal.net.http.Http2Connection$FramesController.processReceivedData(Http2Connection.java:318)
	at java.net.http/jdk.internal.net.http.Http2Connection.asyncReceive(Http2Connection.java:871)
	at java.net.http/jdk.internal.net.http.Http2Connection$Http2TubeSubscriber.processQueue(Http2Connection.java:1840)
	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$LockingRestartableTask.run(SequentialScheduler.java:182)
	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149)
	at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:207)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
	at java.base/java.lang.Thread.run(Thread.java:1474)

対策2: HTTP2を無効化

@SpringBootTest
public class TodoClientTest {
    @Autowired
    TodoClient todoClient;

    @RegisterExtension
    static WireMockExtension wireMock = WireMockExtension.newInstance()
            .options(wireMockConfig()
                    // コレ!!!
                    .http2PlainDisabled(true)
                    .port(9999))
            .build();

これでテストがすべてOKになりました!

Discussion