Spring BootのWebClientのテストにSpring Cloud ContractのWireMockを使う

Spring BootのWebClientのテストにSpring Cloud ContractのWireMockを使ってみたときのメモ。
(ちなみに、RestTemplateのテストで使えるMockRestServiceServerはWebClientをサポートしていなかった)

ソースコード

build.gradle

plugins {
    id 'org.springframework.boot' version '2.4.5'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencyManagement {
    imports {
        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:2020.0.2'
    }
}

dependencies {
    annotationProcessor 'org.projectlombok:lombok'
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'
    testImplementation 'io.projectreactor:reactor-test'
}

test {
    useJUnitPlatform()
}

テスト対象コード

GitHubApiClient.java

@Component
public class GitHubApiClient {
    private final WebClient webClient;

    public GitHubApiClient(WebClient.Builder builder,
                           @Value("${base-url:https://api.github.com}") String baseUrl) {
        webClient = builder.baseUrl(baseUrl).build();
    }

    public Mono<User> getUser(String username) {
        return webClient.get()
                        .uri("/users/{username}", username)
                        .retrieve()
                        .bodyToMono(User.class);
    }
}

User.java

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonNaming(SnakeCaseStrategy.class)
@Data
public class User {
    private long id;
    private String login;
    ...
}

テストコード

GitHubApiClientTest.java

package com.example;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

@SpringBootTest(properties = "base-url=http://localhost:${wiremock.server.port}")
@AutoConfigureWireMock(port = 0)
class GitHubApiClientTest {
    @Autowired
    private GitHubApiClient client;

    @Test
    void getUser() {
        ResponseDefinitionBuilder response =
                aResponse().withBody("{\"login\":\"hirakida\",\"id\":100}")
                           .withHeader(HttpHeaders.CONTENT_TYPE,
                                       MediaType.APPLICATION_JSON_VALUE)
                           .withStatus(HttpStatus.OK.value());
        stubFor(get(urlEqualTo("/users/hirakida")).willReturn(response));

        Mono<User> result = client.getUser("hirakida");

        StepVerifier.create(result)
                    .expectNextMatches(user -> user != null
                                               && "hirakida".equals(user.getLogin())
                                               && user.getId() == 100)
                    .verifyComplete();
    }
}

参考