尝试使用Spring Cloud Contract

总结

    • アプリケーションの改修時に他のアプリケーションへの影響を聞き取り調査をしていることがあるが、システム的に気づける仕組みを作れないかと思っていた時にspring-cloud-contractを見つけたので触ってみた

 

    • gradle使用

 

    • 年齢を受け取ってadult or childを返すアプリケーションでやってみる!

 

    ざっくりとした構成は以下の通り
image.png

可以先表达一下对此的感受

    • producer側のAPIのテストコードを自動生成してくれるから楽

 

    • consumer側の煩雑なスタブ実装がいらなくなるから幸せ!

 

    • 何よりproducer,consumerで期待値を共有できるから安心!!

 

    あとgroovyが書きやすい

可供参考

    • 公式:https://spring.io/projects/spring-cloud-contract

Youtube: https://www.youtube.com/watch?v=sAAklvxmPmk

制作方面的实施

应用程序

import lombok.Data;
import lombok.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
public class ProducerController {
    // curl localhost:8081/checkAge -X POST -H 'Content-Type: application/json' -d '{"age":20}'
    @PostMapping("/checkAge")
    public Mono<Result> checkAge(@RequestBody Person person) {
        return Mono.just(person)
                .map(Person::getAge)
                .map(v -> v >= 20 ? "adult" : "child")
                .map(Result::new);
    }

    @Value
    public static class Result {
        private String result;
    }

    @Data
    public static class Person {
        private int age;
    }
}

创建build.gradle。

    https://start.spring.io/
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.springframework.cloud:spring-cloud-contract-gradle-plugin:2.2.1.RELEASE'
    }
}

plugins {
    id 'org.springframework.boot' version '2.2.4.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
    id 'maven'  // localのrepositoryにjarを登録するために使用
}

apply plugin: 'spring-cloud-contract'

group = 'rhirabay'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 11
targetCompatibility = 11

repositories {
    mavenCentral()
    mavenLocal()
}

ext {
    set('springCloudVersion', "Hoxton.SR1")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'io.projectreactor:reactor-test'
    testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
    testImplementation 'io.rest-assured:spring-web-test-client'

    testCompileOnly('org.projectlombok:lombok')
    testAnnotationProcessor('org.projectlombok:lombok')
    testRuntime('org.junit.jupiter:junit-jupiter-engine')
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

contracts {
    testFramework = org.springframework.cloud.contract.verifier.config.TestFramework.JUNIT5
    testMode = 'WEBTESTCLIENT'       // webfluxを使用している場合はこれ
    baseClassForTests = 'rhirabay.contract.ContractTestBase'   // contractテスト時の抽象クラスを指定する
}

test {
    useJUnitPlatform()
}

创建Contract文件

test/resources/contracts配下に作成
「こんなリクエストに対してこんなレスポンスをお約束しますよ」を記述するファイル
consumer側のテスト時にこれを元にスタブが作成される
producer側のテスト時にこれを元にテストが作成される

package contracts

org.springframework.cloud.contract.spec.Contract.make {
    request {
        description("age is over 20, so expected 'adult'")
        method 'POST'
        url '/checkAge'
        body ([
                age: 20
        ])
        headers {
            contentType('application/json')
        }
    }
    response {
        status 200
        body ([
                result: 'adult'
        ])
        headers {
            contentType('application/json')
        }
    }
}

考试准备

import io.restassured.module.webtestclient.RestAssuredWebTestClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import rhirabay.ProducerController;

@ExtendWith(SpringExtension.class)
@WebFluxTest
public abstract class ContractTestBase {
    @Autowired
    private WebTestClient webTestClient;

    @BeforeEach
    public void setup() {
        RestAssuredWebTestClient.webTestClient(webTestClient);
        RestAssuredWebTestClient.standaloneSetup(new ProducerController());
    }
}

进行考试

testタスクを実行!
テストが通ったらinstallタスクでローカルにstubのjarファイルを保存

先にbootJarをしておかないとコケるかも
jarファイルを確認したければbuild/libs配下に生成されているはず
このjarをconsumer側で読み込むと、先ほど作成したContractファイルを元にスタブが立ち上がる

消费者端的实施

应用程序

import lombok.Data;
import lombok.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@RestController
public class ConsumerController {
    private WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:8081")
            .build();

    // curl localhost:8080/checkAge?age=20
    @GetMapping("/checkAge")
    public Mono<Result> checkAgeLight(@RequestParam int age) {
        return webClient.post()
                .uri("/checkAge")
                .contentType(MediaType.APPLICATION_JSON)
                .body(Mono.just(new Person(age)), Person.class)
                .retrieve()
                .bodyToMono(Result.class);
    }

    @Data
    public static class Result {
        private String result;
    }

    @Value
    public static class Person {
        private int age;
    }
}

构建.gradle文件

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.springframework.cloud:spring-cloud-contract-gradle-plugin:2.2.1.RELEASE'
    }
}

plugins {
    id 'org.springframework.boot' version '2.2.4.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
    id 'maven'
}

group = 'rhirabay'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 11
targetCompatibility = 11

repositories {
    mavenCentral()
    mavenLocal()
}

ext {
    set('springCloudVersion', "Hoxton.SR1")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'io.projectreactor:reactor-test'
    testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'
    testImplementation 'io.rest-assured:spring-web-test-client'


    testCompileOnly('org.projectlombok:lombok')
    testAnnotationProcessor('org.projectlombok:lombok')
    testRuntime('org.junit.jupiter:junit-jupiter-engine')
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

test {
    useJUnitPlatform()
}

创建考试

stubsModeはLOCALを指定

業務で使用するときはNexasとかArtifactoryにあげてrepositoryRootで場所を指定

idsで先ほどローカルにinstallしたライブラリを指定

@ExtendWith(SpringExtension.class)
@WebFluxTest
@AutoConfigureStubRunner(
        stubsMode = StubRunnerProperties.StubsMode.LOCAL,
        ids = "rhirabay:springcloudcontract-producer:+:stubs:8081"
)
public class ConsumerControllerTest {

    @Autowired
    private WebTestClient webClient;

    @Test
    public void test_adult() {
        webClient.get()
                .uri("/checkAge?age={age}", 20)
                .exchange()
                .expectStatus().isOk()
                .expectBody(Result.class).isEqualTo(new Result("adult"));
    }

    @Test
    public void test_child() {
        webClient.get()
                .uri("/checkAge?age={age}", 19)
                .exchange()
                .expectStatus().isOk()
                .expectBody(Result.class).isEqualTo(new Result("child"));

    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Result {
        private String result;
    }
}

开始考试!

testタスクを実行!

问题是什么?

    producer側を変更した時にconsumer側を自動テストするにはどうしたら良いんだろう…
广告
将在 10 秒后关闭
bannerAds