雑記: タイムアウトできなかった話

お気付きの点がありましたらご指摘いただけますと幸いです。

「RestTemplate で外部サーバにアクセスするときに確率的にタイムアウトする」という状況を単体テストで実現したいとします。外部サーバにアクセスするときのテストでは MockRestServiceServer で外部サーバをモックするのでこれを利用するのだろうなあと思って検索すると参考文献 1. にヒットするのでそのベストアンサーを参考に以下のように実装します。しかし、常に失敗すべきこのテストは一向に失敗しないことがわかります。ベストアンサーの回答者が "But, I should warn you," といっているように、MockRestServiceServer を RestTemplate にバインドしたことでもはや元々の RestTemplate の RequestFactory がタイムアウト値ごと置換されていることがわかります。となると質問に対するベストアンサーではないような気がしてならないですが放っておくことにします。

package com.example.bentou;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;

class BentouApplicationTest {
    // RestTemplate を用意 (タイムアウト2秒)
    private RestTemplate restTemplate = new RestTemplateBuilder()
            .setConnectTimeout(Duration.ofSeconds(1))
            .setReadTimeout(Duration.ofSeconds(1))
            .build();

    // ランダムスリープする関数 (失敗させるために長めにスリープ)
    private Random rand = new Random();
    public void randomSleep() {
        int duration = 5 + rand.nextInt(5);
        try { TimeUnit.SECONDS.sleep(duration); } catch (InterruptedException ignored) {}
    }

    // RestTemplate から /abc にアクセスするとランダムスリープしてから "OK" を返すようにモック
    @BeforeEach
    public void setUp() {
        MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
        mockServer.expect(requestTo("/abc")).andRespond(request -> {
            randomSleep();
            return new MockClientHttpResponse("OK".getBytes(), HttpStatus.OK);
        });
    }

    // RestTemplate から /abc にアクセス
    @Test
    public void test() {
        String resp = restTemplate.getForObject("abc", String.class);
        assertEquals("OK", resp);
    }
}

なので MockRestServiceServer を忘れて参考文献 2.〜4. を参考に以下のように実装すると4回目の外部アクセスで失敗するという挙動は実現できます。いい方法かわかりません。これはモックなので RestTemplate 自体のタイムアウトのテストにはなっていないですが、RestTemplate による外部アクセスをサーキットブレーカーでラップしたときにサーキットブレーカーが意図通りの挙動をするかのテストには利用できると思います。というか RestTemplate をわざとタイムアウトさせてサーキットブレーカーの挙動を勉強しようとしたのですがそもそも RestTemplate をタイムアウトさせるのに躓いたという話でした。

package com.example.bentou;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class BentouApplicationTest {
    // RestTemplate を用意 (モック)
    @Mock
    private RestTemplate restTemplate;

    // RestTemplate から /abc にアクセスすると4回目に失敗するようにモック
    @BeforeEach
    public void setUp() {
        when(restTemplate.getForObject("abc", String.class))
                .thenReturn("OK", "OK", "OK")
                .thenThrow(new RestClientException("ERROR"));
    }

    // RestTemplate から /abc にアクセス
    @Test
    public void test() {
        String resp;
        // 最初の3回は成功
        for (int i = 0; i < 3; ++i) {
            resp = restTemplate.getForObject("abc", String.class);
            assertEquals("OK", resp);
        }
        // 4回目は失敗
        try {
            restTemplate.getForObject("abc", String.class);
            fail();
        } catch (RestClientException exception) {
        }
    }
}