Spring Cloud Contract 功能

1. 合同 DSL

Spring Cloud Contract 支持以下语言编写的 DSL:spring-doc.cn

Spring Cloud Contract 支持在单个文件中定义多个 Contract。

以下示例显示了 Contract 定义:spring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    request {
        method 'PUT'
        url '/api/12'
        headers {
            header 'Content-Type': 'application/vnd.org.springframework.cloud.contract.verifier.twitter-places-analyzer.v1+json'
        }
        body '''\
    [{
        "created_at": "Sat Jul 26 09:38:57 +0000 2014",
        "id": 492967299297845248,
        "id_str": "492967299297845248",
        "text": "Gonna see you at Warsaw",
        "place":
        {
            "attributes":{},
            "bounding_box":
            {
                "coordinates":
                    [[
                        [-77.119759,38.791645],
                        [-76.909393,38.791645],
                        [-76.909393,38.995548],
                        [-77.119759,38.995548]
                    ]],
                "type":"Polygon"
            },
            "country":"United States",
            "country_code":"US",
            "full_name":"Washington, DC",
            "id":"01fbe706f872cb32",
            "name":"Washington",
            "place_type":"city",
            "url": "https://api.twitter.com/1/geo/id/01fbe706f872cb32.json"
        }
    }]
'''
    }
    response {
        status OK()
    }
}
YAML
description: Some description
name: some name
priority: 8
ignored: true
request:
  url: /foo
  queryParameters:
    a: b
    b: c
  method: PUT
  headers:
    foo: bar
    fooReq: baz
  body:
    foo: bar
  matchers:
    body:
      - path: $.foo
        type: by_regex
        value: bar
    headers:
      - key: foo
        regex: bar
response:
  status: 200
  headers:
    foo2: bar
    foo3: foo33
    fooRes: baz
  body:
    foo2: bar
    foo3: baz
    nullValue: null
  matchers:
    body:
      - path: $.foo2
        type: by_regex
        value: bar
      - path: $.foo3
        type: by_command
        value: executeMe($it)
      - path: $.nullValue
        type: by_null
        value: null
    headers:
      - key: foo2
        regex: bar
      - key: foo3
        command: andMeToo($it)
Java
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;

import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;

class contract_rest implements Supplier<Collection<Contract>> {

    @Override
    public Collection<Contract> get() {
        return Collections.singletonList(Contract.make(c -> {
            c.description("Some description");
            c.name("some name");
            c.priority(8);
            c.ignored();
            c.request(r -> {
                r.url("/foo", u -> {
                    u.queryParameters(q -> {
                        q.parameter("a", "b");
                        q.parameter("b", "c");
                    });
                });
                r.method(r.PUT());
                r.headers(h -> {
                    h.header("foo", r.value(r.client(r.regex("bar")), r.server("bar")));
                    h.header("fooReq", "baz");
                });
                r.body(ContractVerifierUtil.map().entry("foo", "bar"));
                r.bodyMatchers(m -> {
                    m.jsonPath("$.foo", m.byRegex("bar"));
                });
            });
            c.response(r -> {
                r.fixedDelayMilliseconds(1000);
                r.status(r.OK());
                r.headers(h -> {
                    h.header("foo2", r.value(r.server(r.regex("bar")), r.client("bar")));
                    h.header("foo3", r.value(r.server(r.execute("andMeToo($it)")), r.client("foo33")));
                    h.header("fooRes", "baz");
                });
                r.body(ContractVerifierUtil.map().entry("foo2", "bar").entry("foo3", "baz").entry("nullValue", null));
                r.bodyMatchers(m -> {
                    m.jsonPath("$.foo2", m.byRegex("bar"));
                    m.jsonPath("$.foo3", m.byCommand("executeMe($it)"));
                    m.jsonPath("$.nullValue", m.byNull());
                });
            });
        }));
    }

}
Kotlin
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract
import org.springframework.cloud.contract.spec.withQueryParameters

contract {
    name = "some name"
    description = "Some description"
    priority = 8
    ignored = true
    request {
        url = url("/foo") withQueryParameters  {
            parameter("a", "b")
            parameter("b", "c")
        }
        method = PUT
        headers {
            header("foo", value(client(regex("bar")), server("bar")))
            header("fooReq", "baz")
        }
        body = body(mapOf("foo" to "bar"))
        bodyMatchers {
            jsonPath("$.foo", byRegex("bar"))
        }
    }
    response {
        delay = fixedMilliseconds(1000)
        status = OK
        headers {
            header("foo2", value(server(regex("bar")), client("bar")))
            header("foo3", value(server(execute("andMeToo(\$it)")), client("foo33")))
            header("fooRes", "baz")
        }
        body = body(mapOf(
                "foo" to "bar",
                "foo3" to "baz",
                "nullValue" to null
        ))
        bodyMatchers {
            jsonPath("$.foo2", byRegex("bar"))
            jsonPath("$.foo3", byCommand("executeMe(\$it)"))
            jsonPath("$.nullValue", byNull)
        }
    }
}

您可以使用以下独立的 Maven 命令将合同编译为存根映射:spring-doc.cn

mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert

1.1. 在 Groovy 中约定 DSL

如果您不熟悉 Groovy,请不要担心。您可以在 Groovy DSL 文件。spring-doc.cn

如果您决定用 Groovy 编写合约,如果您还没有使用 Groovy,请不要惊慌 以前。实际上并不需要了解该语言,因为 Contract DSL 只使用 它的一小部分(仅 Literals、Method Calls 和 Closure)。此外,DSL 是静态的 typed,使其无需任何 DSL 本身知识即可程序员可读。spring-doc.cn

请记住,在 Groovy 合约文件中,您必须提供完整的 限定名称添加到类和静态导入中,例如 .您还可以将导入 类 (),然后调用 .Contractmakeorg.springframework.cloud.spec.Contract.make { …​ }Contractimport org.springframework.cloud.spec.ContractContract.make { …​ }

1.2. Java 中的合约 DSL

要在 Java 中编写合同定义,您需要创建一个类,该类实现接口(对于单个合同)或(对于多个合同)。Supplier<Contract>Supplier<Collection<Contract>>spring-doc.cn

您还可以在 (例如) 下编写 Contract 定义,这样就不必修改项目的 Classpath 。在这种情况下,您必须为 Spring Cloud Contract 插件提供 Contract 定义的新位置。src/test/javasrc/test/java/contractsspring-doc.cn

以下示例(在 Maven 和 Gradle 中)在 Contract definitions 下 :src/test/javaspring-doc.cn

Maven 系列
<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <contractsDirectory>src/test/java/contracts</contractsDirectory>
    </configuration>
</plugin>
Gradle
contracts {
    contractsDslDir = new File(project.rootDir, "src/test/java/contracts")
}

1.3. 在 Kotlin 中约定 DSL

要开始使用 Kotlin 编写合约,您需要从(新创建的)Kotlin 脚本文件 () 开始。 与 Java DSL 一样,您可以将 Contract 放在您选择的任何目录中。 默认情况下,Maven 插件将查看目录,而 Gradle 插件将 看看目录。.ktssrc/test/resources/contractssrc/contractTest/resources/contractsspring-doc.cn

从 3.0.0 开始,Gradle 插件也将查看遗留的 目录进行迁移。在此目录中找到 Contract 时,会显示一条警告 将在您的构建过程中记录。src/test/resources/contracts

您需要将依赖项显式传递给您的项目插件设置。 以下示例(在 Maven 和 Gradle 中)展示了如何做到这一点:spring-cloud-contract-spec-kotlinspring-doc.cn

Maven 系列
<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <!-- some config -->
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-contract-spec-kotlin</artifactId>
            <version>${spring-cloud-contract.version}</version>
        </dependency>
    </dependencies>
</plugin>

<dependencies>
        <!-- Remember to add this for the DSL support in the IDE and on the consumer side -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-contract-spec-kotlin</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>
Gradle
buildscript {
    repositories {
        // ...
    }
    dependencies {
        classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${scContractVersion}"
    }
}

dependencies {
    // ...

    // Remember to add this for the DSL support in the IDE and on the consumer side
    testImplementation "org.springframework.cloud:spring-cloud-contract-spec-kotlin"
    // Kotlin versions are very particular down to the patch version. The <kotlin_version> needs to be the same as you have imported for your project.
    testImplementation "org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:<kotlin_version>"
}
请记住,在 Kotlin 脚本文件中,您必须为类提供完全限定名称。 通常,您将按如下方式使用其 Contract 函数:。 您还可以提供对函数 () 的导入,然后调用 .ContractDSLorg.springframework.cloud.contract.spec.ContractDsl.contract { …​ }contractimport org.springframework.cloud.contract.spec.ContractDsl.Companion.contractcontract { …​ }

1.4. YAML 中的合约 DSL

要查看 YAML 协定的架构,请访问 YML 架构页面。spring-doc.cn

1.5. 限制

对验证 JSON 数组大小的支持是实验性的。如果需要帮助, 要打开它,请将以下 System Property 的值设置为 : 。默认情况下,此功能设置为 。 您还可以在插件配置中设置该属性。truespring.cloud.contract.verifier.assert.sizefalseassertJsonSize
由于 JSON 结构可以具有任何形式,因此可能无法对其进行解析 在使用 Groovy DSL 和 中的表示法时正确。那 是您应该使用 Groovy Map 表示法的原因。value(consumer(…​), producer(…​))GString

1.6. 常见的顶级元素

以下部分介绍了最常见的顶级元素:spring-doc.cn

1.6.1. 描述

您可以将 添加到您的合同中。描述是任意文本。这 下面的代码显示了一个示例:descriptionspring-doc.cn

槽的
            org.springframework.cloud.contract.spec.Contract.make {
                description('''
given:
    An input
when:
    Sth happens
then:
    Output
''')
            }
YAML
description: Some description
name: some name
priority: 8
ignored: true
request:
  url: /foo
  queryParameters:
    a: b
    b: c
  method: PUT
  headers:
    foo: bar
    fooReq: baz
  body:
    foo: bar
  matchers:
    body:
      - path: $.foo
        type: by_regex
        value: bar
    headers:
      - key: foo
        regex: bar
response:
  status: 200
  headers:
    foo2: bar
    foo3: foo33
    fooRes: baz
  body:
    foo2: bar
    foo3: baz
    nullValue: null
  matchers:
    body:
      - path: $.foo2
        type: by_regex
        value: bar
      - path: $.foo3
        type: by_command
        value: executeMe($it)
      - path: $.nullValue
        type: by_null
        value: null
    headers:
      - key: foo2
        regex: bar
      - key: foo3
        command: andMeToo($it)
Java
Contract.make(c -> {
    c.description("Some description");
}));
Kotlin
contract {
    description = """
given:
    An input
when:
    Sth happens
then:
    Output
"""
}

1.6.2. 名称

您可以为合同提供名称。假设您提供以下名称:。如果这样做,则自动生成的测试的名称为 。此外,WireMock 存根中的存根名称为 .should register a uservalidate_should_register_a_usershould_register_a_user.jsonspring-doc.cn

您必须确保名称不包含任何使 生成的测试未编译。另外,请记住,如果您为 多个 Contract,则自动生成的测试无法编译,并且生成的存根 相互覆盖。

以下示例演示如何向协定添加名称:spring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    name("some_special_name")
}
YAML
name: some name
Java
Contract.make(c -> {
    c.name("some name");
}));
Kotlin
contract {
    name = "some_special_name"
}

1.6.3. 忽略 Contract

如果要忽略合约,可以在 plugin 配置或在 Contract 本身上设置 property 的 intent 来设置属性。以下内容 示例展示了如何做到这一点:ignoredspring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    ignored()
}
YAML
ignored: true
Java
Contract.make(c -> {
    c.ignored();
}));
Kotlin
contract {
    ignored = true
}

1.6.4. 正在进行的合同

正在进行的合同不会在生产者端生成测试,但允许生成存根。spring-doc.cn

请谨慎使用此功能,因为它可能会导致误报,因为您会生成存根供使用者使用,而实际上没有实施。

如果要设置正在进行的合同,请执行以下操作 示例展示了如何做到这一点:spring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    inProgress()
}
YAML
inProgress: true
Java
Contract.make(c -> {
    c.inProgress();
}));
Kotlin
contract {
    inProgress = true
}

您可以设置 Spring Cloud Contract 插件属性的值,以确保当源中至少有一个正在进行的 Contract 时,您的构建会中断。failOnInProgressspring-doc.cn

1.6.5. 从文件传递值

从 version 开始,您可以从文件中传递值。假设您拥有 项目中的以下资源:1.2.0spring-doc.cn

└── src
    └── test
        └── resources
            └── contracts
                ├── readFromFile.groovy
                ├── request.json
                └── response.json

进一步假设您的 Contract 如下:spring-doc.cn

槽的
/*
 * Copyright 2013-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import org.springframework.cloud.contract.spec.Contract

Contract.make {
    request {
        method('PUT')
        headers {
            contentType(applicationJson())
        }
        body(file("request.json"))
        url("/1")
    }
    response {
        status OK()
        body(file("response.json"))
        headers {
            contentType(applicationJson())
        }
    }
}
YAML
request:
  method: GET
  url: /foo
  bodyFromFile: request.json
response:
  status: 200
  bodyFromFile: response.json
Java
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;

import org.springframework.cloud.contract.spec.Contract;

class contract_rest_from_file implements Supplier<Collection<Contract>> {

    @Override
    public Collection<Contract> get() {
        return Collections.singletonList(Contract.make(c -> {
            c.request(r -> {
                r.url("/foo");
                r.method(r.GET());
                r.body(r.file("request.json"));
            });
            c.response(r -> {
                r.status(r.OK());
                r.body(r.file("response.json"));
            });
        }));
    }

}
Kotlin
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

contract {
    request {
        url = url("/1")
        method = PUT
        headers {
            contentType = APPLICATION_JSON
        }
        body = bodyFromFile("request.json")
    }
    response {
        status = OK
        body = bodyFromFile("response.json")
        headers {
            contentType = APPLICATION_JSON
        }
    }
}

进一步假设 JSON 文件如下所示:spring-doc.cn

request.json
{
  "status": "REQUEST"
}
response.json
{
  "status": "RESPONSE"
}

当 test 或 stub 生成时,和 文件的内容将传递给正文 请求或响应。文件的名称必须是某个位置中的文件 相对于合同所在的文件夹。request.jsonresponse.jsonspring-doc.cn

如果需要以二进制形式传递文件的内容, 您可以使用编码的 DSL 或 YAML 中的字段中的方法。fileAsBytesbodyFromFileAsBytesspring-doc.cn

以下示例演示如何传递二进制文件的内容:spring-doc.cn

槽的
import org.springframework.cloud.contract.spec.Contract

Contract.make {
    request {
        url("/1")
        method(PUT())
        headers {
            contentType(applicationOctetStream())
        }
        body(fileAsBytes("request.pdf"))
    }
    response {
        status 200
        body(fileAsBytes("response.pdf"))
        headers {
            contentType(applicationOctetStream())
        }
    }
}
YAML
request:
  url: /1
  method: PUT
  headers:
    Content-Type: application/octet-stream
  bodyFromFileAsBytes: request.pdf
response:
  status: 200
  bodyFromFileAsBytes: response.pdf
  headers:
    Content-Type: application/octet-stream
Java
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;

import org.springframework.cloud.contract.spec.Contract;

class contract_rest_from_pdf implements Supplier<Collection<Contract>> {

    @Override
    public Collection<Contract> get() {
        return Collections.singletonList(Contract.make(c -> {
            c.request(r -> {
                r.url("/1");
                r.method(r.PUT());
                r.body(r.fileAsBytes("request.pdf"));
                r.headers(h -> {
                    h.contentType(h.applicationOctetStream());
                });
            });
            c.response(r -> {
                r.status(r.OK());
                r.body(r.fileAsBytes("response.pdf"));
                r.headers(h -> {
                    h.contentType(h.applicationOctetStream());
                });
            });
        }));
    }

}
Kotlin
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

contract {
    request {
        url = url("/1")
        method = PUT
        headers {
            contentType = APPLICATION_OCTET_STREAM
        }
        body = bodyFromFileAsBytes("contracts/request.pdf")
    }
    response {
        status = OK
        body = bodyFromFileAsBytes("contracts/response.pdf")
        headers {
            contentType = APPLICATION_OCTET_STREAM
        }
    }
}
每当您想要使用二进制有效负载时,都应该使用此方法。 适用于 HTTP 和消息传递。

1.6.6. 元数据

您可以添加到您的合同中。通过元数据,您可以将配置传递给扩展。您可以在下面找到 使用密钥的示例。它的值是一个 map,其 key 是 WireMock 的对象,value 是 WireMock 的对象。Spring Cloud Contract 能够 使用自定义代码修补生成的存根映射的各个部分。您可能希望这样做以添加 webhook,自定义 延迟或与第三方 WireMock 扩展集成。metadatawiremockstubMappingStubMappingspring-doc.cn

槽的
Contract.make {
    request {
        method GET()
        url '/drunks'
    }
    response {
        status OK()
        body([
            count: 100
        ])
        headers {
            contentType("application/json")
        }
    }
    metadata([
        wiremock: [
            stubMapping: '''\
                {
                    "response" : {
                        "fixedDelayMilliseconds": 2000
                    }
                }
            '''
            ]
    ])
}
YML
name: "should count all frauds"
request:
  method: GET
  url: /yamlfrauds
response:
  status: 200
  body:
    count: 200
  headers:
    Content-Type: application/json
metadata:
  wiremock:
    stubMapping: >
      {
        "response" : {
          "fixedDelayMilliseconds": 2000
        }
      }
Java
Contract.make(c -> {
    c.metadata(MetadataUtil.map().entry("wiremock", ContractVerifierUtil.map().entry("stubMapping",
            "{ \"response\" : { \"fixedDelayMilliseconds\" : 2000 } }")));
}));
kotlin
contract {
    metadata("wiremock" to ("stubmapping" to """
{
  "response" : {
    "fixedDelayMilliseconds": 2000
  }
}"""))
}

在以下部分中,您可以找到支持的元数据条目的示例。spring-doc.cn

元数据 amqp

基于 AMQP 的通信的元数据spring-doc.cn

input:
  messageProperties: null
  connectToBroker:
    additionalOptions: null
    declareQueueWithName: null
outputMessage:
  messageProperties: null
  connectToBroker:
    additionalOptions: null
    declareQueueWithName: null

spring-doc.cn

单击此处展开 JSON 架构:
{
  "type" : "object",
  "id" : "urn:jsonschema:org:springframework:cloud:contract:verifier:messaging:amqp:AmqpMetadata",
  "properties" : {
    "input" : {
      "type" : "object",
      "id" : "urn:jsonschema:org:springframework:cloud:contract:verifier:messaging:amqp:AmqpMetadata:MessageAmqpMetadata",
      "properties" : {
        "messageProperties" : {
          "type" : "object",
          "id" : "urn:jsonschema:org:springframework:amqp:core:MessageProperties",
          "properties" : {
            "headers" : {
              "type" : "object",
              "additionalProperties" : {
                "type" : "object",
                "id" : "urn:jsonschema:java:lang:Object"
              }
            },
            "timestamp" : {
              "type" : "integer",
              "format" : "utc-millisec"
            },
            "messageId" : {
              "type" : "string"
            },
            "userId" : {
              "type" : "string"
            },
            "appId" : {
              "type" : "string"
            },
            "clusterId" : {
              "type" : "string"
            },
            "type" : {
              "type" : "string"
            },
            "correlationId" : {
              "type" : "string"
            },
            "replyTo" : {
              "type" : "string"
            },
            "contentType" : {
              "type" : "string"
            },
            "contentEncoding" : {
              "type" : "string"
            },
            "contentLength" : {
              "type" : "integer"
            },
            "deliveryMode" : {
              "type" : "string",
              "enum" : [ "NON_PERSISTENT", "PERSISTENT" ]
            },
            "expiration" : {
              "type" : "string"
            },
            "priority" : {
              "type" : "integer"
            },
            "redelivered" : {
              "type" : "boolean"
            },
            "receivedExchange" : {
              "type" : "string"
            },
            "receivedRoutingKey" : {
              "type" : "string"
            },
            "receivedUserId" : {
              "type" : "string"
            },
            "deliveryTag" : {
              "type" : "integer"
            },
            "messageCount" : {
              "type" : "integer"
            },
            "consumerTag" : {
              "type" : "string"
            },
            "consumerQueue" : {
              "type" : "string"
            },
            "receivedDelay" : {
              "type" : "integer"
            },
            "receivedDeliveryMode" : {
              "type" : "string",
              "enum" : [ "NON_PERSISTENT", "PERSISTENT" ]
            },
            "finalRetryForMessageWithNoId" : {
              "type" : "boolean"
            },
            "publishSequenceNumber" : {
              "type" : "integer"
            },
            "lastInBatch" : {
              "type" : "boolean"
            },
            "projectionUsed" : {
              "type" : "boolean"
            },
            "inferredArgumentType" : {
              "type" : "object",
              "id" : "urn:jsonschema:java:lang:reflect:Type",
              "properties" : {
                "typeName" : {
                  "type" : "string"
                }
              }
            },
            "targetMethod" : {
              "type" : "object",
              "id" : "urn:jsonschema:java:lang:reflect:Method",
              "properties" : {
                "parameters" : {
                  "type" : "array",
                  "items" : {
                    "type" : "object",
                    "id" : "urn:jsonschema:java:lang:reflect:Parameter",
                    "properties" : {
                      "name" : {
                        "type" : "string"
                      },
                      "modifiers" : {
                        "type" : "integer"
                      },
                      "declaredAnnotations" : {
                        "type" : "array",
                        "items" : {
                          "type" : "object",
                          "id" : "urn:jsonschema:java:lang:annotation:Annotation"
                        }
                      },
                      "synthetic" : {
                        "type" : "boolean"
                      },
                      "annotations" : {
                        "type" : "array",
                        "items" : {
                          "type" : "object",
                          "$ref" : "urn:jsonschema:java:lang:annotation:Annotation"
                        }
                      },
                      "type" : {
                        "type" : "string"
                      },
                      "annotatedType" : {
                        "type" : "object",
                        "id" : "urn:jsonschema:java:lang:reflect:AnnotatedType",
                        "properties" : {
                          "annotatedOwnerType" : {
                            "type" : "object",
                            "$ref" : "urn:jsonschema:java:lang:reflect:AnnotatedType"
                          },
                          "type" : {
                            "type" : "object",
                            "$ref" : "urn:jsonschema:java:lang:reflect:Type"
                          },
                          "annotations" : {
                            "type" : "array",
                            "items" : {
                              "type" : "object",
                              "$ref" : "urn:jsonschema:java:lang:annotation:Annotation"
                            }
                          },
                          "declaredAnnotations" : {
                            "type" : "array",
                            "items" : {
                              "type" : "object",
                              "$ref" : "urn:jsonschema:java:lang:annotation:Annotation"
                            }
                          }
                        }
                      },
                      "namePresent" : {
                        "type" : "boolean"
                      },
                      "declaringExecutable" : {
                        "type" : "object",
                        "id" : "urn:jsonschema:java:lang:reflect:Executable",
                        "properties" : {
                          "parameters" : {
                            "type" : "array",
                            "items" : {
                              "type" : "object",
                              "$ref" : "urn:jsonschema:java:lang:reflect:Parameter"
                            }
                          },
                          "declaredAnnotations" : {
                            "type" : "array",
                            "items" : {
                              "type" : "object",
                              "$ref" : "urn:jsonschema:java:lang:annotation:Annotation"
                            }
                          },
                          "name" : {
                            "type" : "string"
                          },
                          "modifiers" : {
                            "type" : "integer"
                          },
                          "synthetic" : {
                            "type" : "boolean"
                          },
                          "typeParameters" : {
                            "type" : "array",
                            "items" : {
                              "type" : "object",
                              "id" : "urn:jsonschema:java:lang:reflect:TypeVariable<java:lang:Object>",
                              "properties" : {
                                "bounds" : {
                                  "type" : "array",
                                  "items" : {
                                    "type" : "object",
                                    "$ref" : "urn:jsonschema:java:lang:reflect:Type"
                                  }
                                },
                                "annotatedBounds" : {
                                  "type" : "array",
                                  "items" : {
                                    "type" : "object",
                                    "$ref" : "urn:jsonschema:java:lang:reflect:AnnotatedType"
                                  }
                                },
                                "genericDeclaration" : {
                                  "type" : "object",
                                  "$ref" : "urn:jsonschema:java:lang:Object"
                                },
                                "name" : {
                                  "type" : "string"
                                },
                                "typeName" : {
                                  "type" : "string"
                                },
                                "annotations" : {
                                  "type" : "array",
                                  "items" : {
                                    "type" : "object",
                                    "$ref" : "urn:jsonschema:java:lang:annotation:Annotation"
                                  }
                                },
                                "declaredAnnotations" : {
                                  "type" : "array",
                                  "items" : {
                                    "type" : "object",
                                    "$ref" : "urn:jsonschema:java:lang:annotation:Annotation"
                                  }
                                }
                              }
                            }
                          },
                          "declaringClass" : {
                            "type" : "string"
                          },
                          "parameterTypes" : {
                            "type" : "array",
                            "items" : {
                              "type" : "string"
                            }
                          },
                          "varArgs" : {
                            "type" : "boolean"
                          },
                          "annotatedParameterTypes" : {
                            "type" : "array",
                            "items" : {
                              "type" : "object",
                              "$ref" : "urn:jsonschema:java:lang:reflect:AnnotatedType"
                            }
                          },
                          "parameterCount" : {
                            "type" : "integer"
                          },
                          "parameterAnnotations" : {
                            "type" : "array",
                            "items" : {
                              "type" : "array",
                              "items" : {
                                "type" : "object",
                                "$ref" : "urn:jsonschema:java:lang:annotation:Annotation"
                              }
                            }
                          },
                          "genericParameterTypes" : {
                            "type" : "array",
                            "items" : {
                              "type" : "object",
                              "$ref" : "urn:jsonschema:java:lang:reflect:Type"
                            }
                          },
                          "exceptionTypes" : {
                            "type" : "array",
                            "items" : {
                              "type" : "string"
                            }
                          },
                          "genericExceptionTypes" : {
                            "type" : "array",
                            "items" : {
                              "type" : "object",
                              "$ref" : "urn:jsonschema:java:lang:reflect:Type"
                            }
                          },
                          "annotatedReturnType" : {
                            "type" : "object",
                            "$ref" : "urn:jsonschema:java:lang:reflect:AnnotatedType"
                          },
                          "annotatedReceiverType" : {
                            "type" : "object",
                            "$ref" : "urn:jsonschema:java:lang:reflect:AnnotatedType"
                          },
                          "annotatedExceptionTypes" : {
                            "type" : "array",
                            "items" : {
                              "type" : "object",
                              "$ref" : "urn:jsonschema:java:lang:reflect:AnnotatedType"
                            }
                          },
                          "annotations" : {
                            "type" : "array",
                            "items" : {
                              "type" : "object",
                              "$ref" : "urn:jsonschema:java:lang:annotation:Annotation"
                            }
                          },
                          "accessible" : {
                            "type" : "boolean"
                          }
                        }
                      },
                      "parameterizedType" : {
                        "type" : "object",
                        "$ref" : "urn:jsonschema:java:lang:reflect:Type"
                      },
                      "implicit" : {
                        "type" : "boolean"
                      },
                      "varArgs" : {
                        "type" : "boolean"
                      }
                    }
                  }
                },
                "declaredAnnotations" : {
                  "type" : "array",
                  "items" : {
                    "type" : "object",
                    "$ref" : "urn:jsonschema:java:lang:annotation:Annotation"
                  }
                },
                "name" : {
                  "type" : "string"
                },
                "returnType" : {
                  "type" : "string"
                },
                "parameterTypes" : {
                  "type" : "array",
                  "items" : {
                    "type" : "string"
                  }
                },
                "exceptionTypes" : {
                  "type" : "array",
                  "items" : {
                    "type" : "string"
                  }
                },
                "modifiers" : {
                  "type" : "integer"
                },
                "annotations" : {
                  "type" : "array",
                  "items" : {
                    "type" : "object",
                    "$ref" : "urn:jsonschema:java:lang:annotation:Annotation"
                  }
                },
                "parameterAnnotations" : {
                  "type" : "array",
                  "items" : {
                    "type" : "array",
                    "items" : {
                      "type" : "object",
                      "$ref" : "urn:jsonschema:java:lang:annotation:Annotation"
                    }
                  }
                },
                "synthetic" : {
                  "type" : "boolean"
                },
                "typeParameters" : {
                  "type" : "array",
                  "items" : {
                    "type" : "object",
                    "id" : "urn:jsonschema:java:lang:reflect:TypeVariable<java:lang:reflect:Method>",
                    "properties" : {
                      "bounds" : {
                        "type" : "array",
                        "items" : {
                          "type" : "object",
                          "$ref" : "urn:jsonschema:java:lang:reflect:Type"
                        }
                      },
                      "annotatedBounds" : {
                        "type" : "array",
                        "items" : {
                          "type" : "object",
                          "$ref" : "urn:jsonschema:java:lang:reflect:AnnotatedType"
                        }
                      },
                      "genericDeclaration" : {
                        "type" : "object",
                        "$ref" : "urn:jsonschema:java:lang:reflect:Method"
                      },
                      "name" : {
                        "type" : "string"
                      },
                      "typeName" : {
                        "type" : "string"
                      },
                      "annotations" : {
                        "type" : "array",
                        "items" : {
                          "type" : "object",
                          "$ref" : "urn:jsonschema:java:lang:annotation:Annotation"
                        }
                      },
                      "declaredAnnotations" : {
                        "type" : "array",
                        "items" : {
                          "type" : "object",
                          "$ref" : "urn:jsonschema:java:lang:annotation:Annotation"
                        }
                      }
                    }
                  }
                },
                "declaringClass" : {
                  "type" : "string"
                },
                "accessible" : {
                  "type" : "boolean"
                },
                "varArgs" : {
                  "type" : "boolean"
                },
                "parameterCount" : {
                  "type" : "integer"
                },
                "genericReturnType" : {
                  "type" : "object",
                  "$ref" : "urn:jsonschema:java:lang:reflect:Type"
                },
                "genericParameterTypes" : {
                  "type" : "array",
                  "items" : {
                    "type" : "object",
                    "$ref" : "urn:jsonschema:java:lang:reflect:Type"
                  }
                },
                "genericExceptionTypes" : {
                  "type" : "array",
                  "items" : {
                    "type" : "object",
                    "$ref" : "urn:jsonschema:java:lang:reflect:Type"
                  }
                },
                "bridge" : {
                  "type" : "boolean"
                },
                "default" : {
                  "type" : "boolean"
                },
                "defaultValue" : {
                  "type" : "object",
                  "$ref" : "urn:jsonschema:java:lang:Object"
                },
                "annotatedReturnType" : {
                  "type" : "object",
                  "$ref" : "urn:jsonschema:java:lang:reflect:AnnotatedType"
                },
                "annotatedParameterTypes" : {
                  "type" : "array",
                  "items" : {
                    "type" : "object",
                    "$ref" : "urn:jsonschema:java:lang:reflect:AnnotatedType"
                  }
                },
                "annotatedReceiverType" : {
                  "type" : "object",
                  "$ref" : "urn:jsonschema:java:lang:reflect:AnnotatedType"
                },
                "annotatedExceptionTypes" : {
                  "type" : "array",
                  "items" : {
                    "type" : "object",
                    "$ref" : "urn:jsonschema:java:lang:reflect:AnnotatedType"
                  }
                }
              }
            },
            "targetBean" : {
              "type" : "object",
              "$ref" : "urn:jsonschema:java:lang:Object"
            },
            "replyToAddress" : {
              "type" : "object",
              "id" : "urn:jsonschema:org:springframework:amqp:core:Address",
              "properties" : {
                "exchangeName" : {
                  "type" : "string"
                },
                "routingKey" : {
                  "type" : "string"
                }
              }
            },
            "delay" : {
              "type" : "integer"
            },
            "xdeathHeader" : {
              "type" : "array",
              "items" : {
                "type" : "object",
                "additionalProperties" : {
                  "type" : "object",
                  "$ref" : "urn:jsonschema:java:lang:Object"
                }
              }
            }
          }
        },
        "connectToBroker" : {
          "type" : "object",
          "id" : "urn:jsonschema:org:springframework:cloud:contract:verifier:messaging:amqp:AmqpMetadata:ConnectToBroker",
          "properties" : {
            "additionalOptions" : {
              "type" : "string"
            },
            "declareQueueWithName" : {
              "type" : "string"
            }
          }
        }
      }
    },
    "outputMessage" : {
      "type" : "object",
      "$ref" : "urn:jsonschema:org:springframework:cloud:contract:verifier:messaging:amqp:AmqpMetadata:MessageAmqpMetadata"
    }
  }
}

spring-doc.cn

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:spring-doc.cn

  • org.springframework.cloud.contract.verifier.messaging.amqp.AmqpMetadataspring-doc.cn

  • org.springframework.amqp.core.MessagePropertiesspring-doc.cn

元数据独立

用于独立通信的元数据 - 使用正在运行的中间件spring-doc.cn

setup:
  options: null
input:
  additionalOptions: null
outputMessage:
  additionalOptions: null

spring-doc.cn

单击此处展开 JSON 架构:
{
  "type" : "object",
  "id" : "urn:jsonschema:org:springframework:cloud:contract:verifier:messaging:camel:StandaloneMetadata",
  "properties" : {
    "setup" : {
      "type" : "object",
      "id" : "urn:jsonschema:org:springframework:cloud:contract:verifier:messaging:camel:StandaloneMetadata:SetupMetadata",
      "properties" : {
        "options" : {
          "type" : "string"
        }
      }
    },
    "input" : {
      "type" : "object",
      "id" : "urn:jsonschema:org:springframework:cloud:contract:verifier:messaging:camel:StandaloneMetadata:MessageMetadata",
      "properties" : {
        "additionalOptions" : {
          "type" : "string"
        }
      }
    },
    "outputMessage" : {
      "type" : "object",
      "$ref" : "urn:jsonschema:org:springframework:cloud:contract:verifier:messaging:camel:StandaloneMetadata:MessageMetadata"
    }
  }
}

spring-doc.cn

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:spring-doc.cn

  • org.springframework.cloud.contract.verifier.messaging.camel.StandaloneMetadataspring-doc.cn

元数据 verifierHttp

框架使用的元数据条目spring-doc.cn

scheme: "HTTP"
protocol: "HTTP_1_1"

spring-doc.cn

单击此处展开 JSON 架构:
{
  "type" : "object",
  "id" : "urn:jsonschema:org:springframework:cloud:contract:verifier:http:ContractVerifierHttpMetaData",
  "properties" : {
    "scheme" : {
      "type" : "string",
      "enum" : [ "HTTP", "HTTPS" ]
    },
    "protocol" : {
      "type" : "string",
      "enum" : [ "HTTP_1_0", "HTTP_1_1", "HTTP_2", "H2_PRIOR_KNOWLEDGE", "QUIC" ]
    }
  }
}

spring-doc.cn

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:spring-doc.cn

  • org.springframework.cloud.contract.verifier.http.ContractVerifierHttpMetaDataspring-doc.cn

元数据 kafka

基于 Kafka 的通信的元数据spring-doc.cn

input:
  connectToBroker:
    additionalOptions: null
outputMessage:
  connectToBroker:
    additionalOptions: null

spring-doc.cn

单击此处展开 JSON 架构:
{
  "type" : "object",
  "id" : "urn:jsonschema:org:springframework:cloud:contract:verifier:messaging:kafka:KafkaMetadata",
  "properties" : {
    "input" : {
      "type" : "object",
      "id" : "urn:jsonschema:org:springframework:cloud:contract:verifier:messaging:kafka:KafkaMetadata:MessageKafkaMetadata",
      "properties" : {
        "connectToBroker" : {
          "type" : "object",
          "id" : "urn:jsonschema:org:springframework:cloud:contract:verifier:messaging:kafka:KafkaMetadata:ConnectToBroker",
          "properties" : {
            "additionalOptions" : {
              "type" : "string"
            }
          }
        }
      }
    },
    "outputMessage" : {
      "type" : "object",
      "$ref" : "urn:jsonschema:org:springframework:cloud:contract:verifier:messaging:kafka:KafkaMetadata:MessageKafkaMetadata"
    }
  }
}

spring-doc.cn

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:spring-doc.cn

  • org.springframework.cloud.contract.verifier.messaging.kafka.KafkaMetadataspring-doc.cn

元数据 wiremock

用于扩展 WireMock 存根的元数据。spring-doc.cn

StubMapping 可以是以下类之一 [, , ]。请检查 wiremock.org/docs/stubbing/ 以了解有关 StubMapping 类属性的更多信息。StringStubMappingMapspring-doc.cn

stubMapping: null

spring-doc.cn

单击此处展开 JSON 架构:
{
  "type" : "object",
  "id" : "urn:jsonschema:org:springframework:cloud:contract:verifier:wiremock:WireMockMetaData",
  "properties" : {
    "stubMapping" : {
      "type" : "object",
      "id" : "urn:jsonschema:java:lang:Object"
    }
  }
}

spring-doc.cn

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:spring-doc.cn

  • org.springframework.cloud.contract.verifier.wiremock.WireMockMetaDataspring-doc.cn

  • com.github.tomakehurst.wiremock.stubbing.StubMappingspring-doc.cn

元数据验证器

框架使用的元数据条目spring-doc.cn

tool: null

spring-doc.cn

单击此处展开 JSON 架构:
{
  "type" : "object",
  "id" : "urn:jsonschema:org:springframework:cloud:contract:verifier:dsl:ContractVerifierMetadata",
  "properties" : {
    "tool" : {
      "type" : "string"
    }
  }
}

spring-doc.cn

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:spring-doc.cn

  • org.springframework.cloud.contract.verifier.dsl.ContractVerifierMetadataspring-doc.cn

元数据 verifierMessage

框架使用的内部元数据条目,与消息传递相关spring-doc.cn

messageType: null

spring-doc.cn

单击此处展开 JSON 架构:
{
  "type" : "object",
  "id" : "urn:jsonschema:org:springframework:cloud:contract:verifier:messaging:internal:ContractVerifierMessageMetadata",
  "properties" : {
    "messageType" : {
      "type" : "string",
      "enum" : [ "SETUP", "INPUT", "OUTPUT" ]
    }
  }
}

spring-doc.cn

如果您有兴趣了解有关类型及其属性的更多信息,请查看以下类:spring-doc.cn

  • org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessageMetadataspring-doc.cn

2. HTTP 的契约

Spring Cloud Contract 允许您验证使用 REST 或 HTTP 作为 通讯方式。Spring Cloud Contract 验证,对于与 criteria 中,服务器会提供一个响应,该响应位于 遵守合同的一部分。随后,这些合约被用来 生成 WireMock 存根,对于与提供条件匹配的任何请求,该存根提供 适当的回应。requestresponsespring-doc.cn

2.1. HTTP 顶级元素

您可以在合约定义的顶级闭包中调用以下方法:spring-doc.cn

以下示例演示如何定义 HTTP 请求协定:spring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    // Definition of HTTP request part of the contract
    // (this can be a valid request or invalid depending
    // on type of contract being specified).
    request {
        method GET()
        url "/foo"
        //...
    }

    // Definition of HTTP response part of the contract
    // (a service implementing this contract should respond
    // with following response after receiving request
    // specified in "request" part above).
    response {
        status 200
        //...
    }

    // Contract priority, which can be used for overriding
    // contracts (1 is highest). Priority is optional.
    priority 1
}
YAML
priority: 8
request:
...
response:
...
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
    // Definition of HTTP request part of the contract
    // (this can be a valid request or invalid depending
    // on type of contract being specified).
    c.request(r -> {
        r.method(r.GET());
        r.url("/foo");
        // ...
    });

    // Definition of HTTP response part of the contract
    // (a service implementing this contract should respond
    // with following response after receiving request
    // specified in "request" part above).
    c.response(r -> {
        r.status(200);
        // ...
    });

    // Contract priority, which can be used for overriding
    // contracts (1 is highest). Priority is optional.
    c.priority(1);
});
Kotlin
contract {
    // Definition of HTTP request part of the contract
    // (this can be a valid request or invalid depending
    // on type of contract being specified).
    request {
        method = GET
        url = url("/foo")
        // ...
    }

    // Definition of HTTP response part of the contract
    // (a service implementing this contract should respond
    // with following response after receiving request
    // specified in "request" part above).
    response {
        status = OK
        // ...
    }

    // Contract priority, which can be used for overriding
    // contracts (1 is highest). Priority is optional.
    priority = 1
}
如果你想让你的合约具有更高的优先级, 您需要将较小的数字传递给 tag 或 method。例如,带有 值 of 的优先级高于值为 .prioritypriority5priority10

2.2. HTTP 请求

HTTP 协议仅要求在请求中指定方法和 URL。这 在合同的请求定义中,相同的信息是必需的。spring-doc.cn

以下示例显示了请求的协定:spring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    request {
        // HTTP request method (GET/POST/PUT/DELETE).
        method 'GET'

        // Path component of request URL is specified as follows.
        urlPath('/users')
    }

    response {
        //...
        status 200
    }
}
YAML
method: PUT
url: /foo
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
    c.request(r -> {
        // HTTP request method (GET/POST/PUT/DELETE).
        r.method("GET");

        // Path component of request URL is specified as follows.
        r.urlPath("/users");
    });

    c.response(r -> {
        // ...
        r.status(200);
    });
});
Kotlin
contract {
    request {
        // HTTP request method (GET/POST/PUT/DELETE).
        method = method("GET")

        // Path component of request URL is specified as follows.
        urlPath = path("/users")
    }
    response {
        // ...
        status = code(200)
    }
}

您可以指定 absolute 而不是 relative ,但使用 is 推荐的方法,因为这样做会使测试独立于主机。urlurlPathspring-doc.cn

以下示例使用 :urlspring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    request {
        method 'GET'

        // Specifying `url` and `urlPath` in one contract is illegal.
        url('http://localhost:8888/users')
    }

    response {
        //...
        status 200
    }
}
YAML
request:
  method: PUT
  urlPath: /foo
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
    c.request(r -> {
        r.method("GET");

        // Specifying `url` and `urlPath` in one contract is illegal.
        r.url("http://localhost:8888/users");
    });

    c.response(r -> {
        // ...
        r.status(200);
    });
});
Kotlin
contract {
    request {
        method = GET

        // Specifying `url` and `urlPath` in one contract is illegal.
        url("http://localhost:8888/users")
    }
    response {
        // ...
        status = OK
    }
}

request可能包含查询参数,如下例(使用 )所示:urlPathspring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    request {
        //...
        method GET()

        urlPath('/users') {

            // Each parameter is specified in form
            // `'paramName' : paramValue` where parameter value
            // may be a simple literal or one of matcher functions,
            // all of which are used in this example.
            queryParameters {

                // If a simple literal is used as value
                // default matcher function is used (equalTo)
                parameter 'limit': 100

                // `equalTo` function simply compares passed value
                // using identity operator (==).
                parameter 'filter': equalTo("email")

                // `containing` function matches strings
                // that contains passed substring.
                parameter 'gender': value(consumer(containing("[mf]")), producer('mf'))

                // `matching` function tests parameter
                // against passed regular expression.
                parameter 'offset': value(consumer(matching("[0-9]+")), producer(123))

                // `notMatching` functions tests if parameter
                // does not match passed regular expression.
                parameter 'loginStartsWith': value(consumer(notMatching(".{0,2}")), producer(3))
            }
        }

        //...
    }

    response {
        //...
        status 200
    }
}
YAML
request:
...
queryParameters:
  a: b
  b: c
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
    c.request(r -> {
        // ...
        r.method(r.GET());

        r.urlPath("/users", u -> {

            // Each parameter is specified in form
            // `'paramName' : paramValue` where parameter value
            // may be a simple literal or one of matcher functions,
            // all of which are used in this example.
            u.queryParameters(q -> {

                // If a simple literal is used as value
                // default matcher function is used (equalTo)
                q.parameter("limit", 100);

                // `equalTo` function simply compares passed value
                // using identity operator (==).
                q.parameter("filter", r.equalTo("email"));

                // `containing` function matches strings
                // that contains passed substring.
                q.parameter("gender", r.value(r.consumer(r.containing("[mf]")), r.producer("mf")));

                // `matching` function tests parameter
                // against passed regular expression.
                q.parameter("offset", r.value(r.consumer(r.matching("[0-9]+")), r.producer(123)));

                // `notMatching` functions tests if parameter
                // does not match passed regular expression.
                q.parameter("loginStartsWith", r.value(r.consumer(r.notMatching(".{0,2}")), r.producer(3)));
            });
        });

        // ...
    });

    c.response(r -> {
        // ...
        r.status(200);
    });
});
Kotlin
contract {
    request {
        // ...
        method = GET

        // Each parameter is specified in form
        // `'paramName' : paramValue` where parameter value
        // may be a simple literal or one of matcher functions,
        // all of which are used in this example.
        urlPath = path("/users") withQueryParameters {
            // If a simple literal is used as value
            // default matcher function is used (equalTo)
            parameter("limit", 100)

            // `equalTo` function simply compares passed value
            // using identity operator (==).
            parameter("filter", equalTo("email"))

            // `containing` function matches strings
            // that contains passed substring.
            parameter("gender", value(consumer(containing("[mf]")), producer("mf")))

            // `matching` function tests parameter
            // against passed regular expression.
            parameter("offset", value(consumer(matching("[0-9]+")), producer(123)))

            // `notMatching` functions tests if parameter
            // does not match passed regular expression.
            parameter("loginStartsWith", value(consumer(notMatching(".{0,2}")), producer(3)))
        }

        // ...
    }
    response {
        // ...
        status = code(200)
    }
}

request可以包含其他请求标头,如下例所示:spring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    request {
        //...
        method GET()
        url "/foo"

        // Each header is added in form `'Header-Name' : 'Header-Value'`.
        // there are also some helper methods
        headers {
            header 'key': 'value'
            contentType(applicationJson())
        }

        //...
    }

    response {
        //...
        status 200
    }
}
YAML
request:
...
headers:
  foo: bar
  fooReq: baz
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
    c.request(r -> {
        // ...
        r.method(r.GET());
        r.url("/foo");

        // Each header is added in form `'Header-Name' : 'Header-Value'`.
        // there are also some helper methods
        r.headers(h -> {
            h.header("key", "value");
            h.contentType(h.applicationJson());
        });

        // ...
    });

    c.response(r -> {
        // ...
        r.status(200);
    });
});
Kotlin
contract {
    request {
        // ...
        method = GET
        url = url("/foo")

        // Each header is added in form `'Header-Name' : 'Header-Value'`.
        // there are also some helper variables
        headers {
            header("key", "value")
            contentType = APPLICATION_JSON
        }

        // ...
    }
    response {
        // ...
        status = OK
    }
}

request可能包含其他请求 Cookie,如下例所示:spring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    request {
        //...
        method GET()
        url "/foo"

        // Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
        // there are also some helper methods
        cookies {
            cookie 'key': 'value'
            cookie('another_key', 'another_value')
        }

        //...
    }

    response {
        //...
        status 200
    }
}
YAML
request:
...
cookies:
  foo: bar
  fooReq: baz
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
    c.request(r -> {
        // ...
        r.method(r.GET());
        r.url("/foo");

        // Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
        // there are also some helper methods
        r.cookies(ck -> {
            ck.cookie("key", "value");
            ck.cookie("another_key", "another_value");
        });

        // ...
    });

    c.response(r -> {
        // ...
        r.status(200);
    });
});
Kotlin
contract {
    request {
        // ...
        method = GET
        url = url("/foo")

        // Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
        // there are also some helper methods
        cookies {
            cookie("key", "value")
            cookie("another_key", "another_value")
        }

        // ...
    }

    response {
        // ...
        status = code(200)
    }
}

request可能包含请求正文,如下例所示:spring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    request {
        //...
        method GET()
        url "/foo"

        // Currently only JSON format of request body is supported.
        // Format will be determined from a header or body's content.
        body '''{ "login" : "john", "name": "John The Contract" }'''
    }

    response {
        //...
        status 200
    }
}
YAML
request:
...
body:
  foo: bar
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
    c.request(r -> {
        // ...
        r.method(r.GET());
        r.url("/foo");

        // Currently only JSON format of request body is supported.
        // Format will be determined from a header or body's content.
        r.body("{ \"login\" : \"john\", \"name\": \"John The Contract\" }");
    });

    c.response(r -> {
        // ...
        r.status(200);
    });
});
Kotlin
contract {
    request {
        // ...
        method = GET
        url = url("/foo")

        // Currently only JSON format of request body is supported.
        // Format will be determined from a header or body's content.
        body = body("{ \"login\" : \"john\", \"name\": \"John The Contract\" }")
    }
    response {
        // ...
        status = OK
    }
}

request可以包含 multipart 元素。要包含多部分元素,请使用 method/section,如下例所示:multipartspring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make {
    request {
        method 'PUT'
        url '/multipart'
        headers {
            contentType('multipart/form-data;boundary=AaB03x')
        }
        multipart(
                // key (parameter name), value (parameter value) pair
                formParameter: $(c(regex('".+"')), p('"formParameterValue"')),
                someBooleanParameter: $(c(regex(anyBoolean())), p('true')),
                // a named parameter (e.g. with `file` name) that represents file with
                // `name` and `content`. You can also call `named("fileName", "fileContent")`
                file: named(
                        // name of the file
                        name: $(c(regex(nonEmpty())), p('filename.csv')),
                        // content of the file
                        content: $(c(regex(nonEmpty())), p('file content')),
                        // content type for the part
                        contentType: $(c(regex(nonEmpty())), p('application/json')))
        )
    }
    response {
        status OK()
    }
}
org.springframework.cloud.contract.spec.Contract contractDsl = org.springframework.cloud.contract.spec.Contract.make {
    request {
        method "PUT"
        url "/multipart"
        headers {
            contentType('multipart/form-data;boundary=AaB03x')
        }
        multipart(
                file: named(
                        name: value(stub(regex('.+')), test('file')),
                        content: value(stub(regex('.+')), test([100, 117, 100, 97] as byte[]))
                )
        )
    }
    response {
        status 200
    }
}
YAML
request:
  method: PUT
  url: /multipart
  headers:
    Content-Type: multipart/form-data;boundary=AaB03x
  multipart:
    params:
      # key (parameter name), value (parameter value) pair
      formParameter: '"formParameterValue"'
      someBooleanParameter: true
    named:
      - paramName: file
        fileName: filename.csv
        fileContent: file content
  matchers:
    multipart:
      params:
        - key: formParameter
          regex: ".+"
        - key: someBooleanParameter
          predefined: any_boolean
      named:
        - paramName: file
          fileName:
            predefined: non_empty
          fileContent:
            predefined: non_empty
response:
  status: 200
Java
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.DslProperty;
import org.springframework.cloud.contract.spec.internal.Request;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;

class contract_multipart implements Supplier<Collection<Contract>> {

    private static Map<String, DslProperty> namedProps(Request r) {
        Map<String, DslProperty> map = new HashMap<>();
        // name of the file
        map.put("name", r.$(r.c(r.regex(r.nonEmpty())), r.p("filename.csv")));
        // content of the file
        map.put("content", r.$(r.c(r.regex(r.nonEmpty())), r.p("file content")));
        // content type for the part
        map.put("contentType", r.$(r.c(r.regex(r.nonEmpty())), r.p("application/json")));
        return map;
    }

    @Override
    public Collection<Contract> get() {
        return Collections.singletonList(Contract.make(c -> {
            c.request(r -> {
                r.method("PUT");
                r.url("/multipart");
                r.headers(h -> {
                    h.contentType("multipart/form-data;boundary=AaB03x");
                });
                r.multipart(ContractVerifierUtil.map()
                        // key (parameter name), value (parameter value) pair
                        .entry("formParameter", r.$(r.c(r.regex("\".+\"")), r.p("\"formParameterValue\"")))
                        .entry("someBooleanParameter", r.$(r.c(r.regex(r.anyBoolean())), r.p("true")))
                        // a named parameter (e.g. with `file` name) that represents file
                        // with
                        // `name` and `content`. You can also call `named("fileName",
                        // "fileContent")`
                        .entry("file", r.named(namedProps(r))));
            });
            c.response(r -> {
                r.status(r.OK());
            });
        }));
    }

}
Kotlin
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

contract {
    request {
        method = PUT
        url = url("/multipart")
        multipart {
            field("formParameter", value(consumer(regex("\".+\"")), producer("\"formParameterValue\"")))
            field("someBooleanParameter", value(consumer(anyBoolean), producer("true")))
            field("file",
                named(
                    // name of the file
                    value(consumer(regex(nonEmpty)), producer("filename.csv")),
                    // content of the file
                    value(consumer(regex(nonEmpty)), producer("file content")),
                    // content type for the part
                    value(consumer(regex(nonEmpty)), producer("application/json"))
                )
            )
        }
        headers {
            contentType = "multipart/form-data;boundary=AaB03x"
        }
    }
    response {
        status = OK
    }
}

在前面的示例中,我们通过以下两种方式之一定义了参数:spring-doc.cn

编码 DSL
  • 直接使用 map 表示法,其中值可以是动态属性(如 )。formParameter: $(consumer(…​), producer(…​))spring-doc.cn

  • 通过使用允许您设置命名参数的方法。命名参数 可以设置 a 和 .您可以使用带有两个参数的方法 (如 )或使用映射表示法(如 .named(…​)namecontentnamed("fileName", "fileContent")named(name: "fileName", content: "fileContent")spring-doc.cn

YAML
  • multipart 参数在 节中设置。multipart.paramsspring-doc.cn

  • 命名参数(给定参数名称的 和 ) 可以在 部分中设置。该部分包含 的 (参数名称)、(文件名)、(文件的内容) 字段。fileNamefileContentmultipart.namedparamNamefileNamefileContentspring-doc.cn

  • 可以在 section 中设置 dynamic bits。matchers.multipartspring-doc.cn

    • 对于参数,请使用 section which can accept 或正则表达式。paramsregexpredefinedspring-doc.cn

    • 对于命名参数,请使用您首先 使用 定义参数名称 。然后,您可以将 在 a 或 或正则表达式中对 or 进行参数化。namedparamNamefileNamefileContentregexpredefinedspring-doc.cn

对于该部分,您始终必须添加一对调用。只需设置 DSL 属性,例如 因为 just 或 just 不会奏效。 有关详细信息,请查看此问题named(…​)value(producer(…​), consumer(…​))value(producer(…​))file(…​)

在前面示例中的 Contract 中,生成的 test 和 stub 如下所示:spring-doc.cn

测试
// given:
  MockMvcRequestSpecification request = given()
    .header("Content-Type", "multipart/form-data;boundary=AaB03x")
    .param("formParameter", "\"formParameterValue\"")
    .param("someBooleanParameter", "true")
    .multiPart("file", "filename.csv", "file content".getBytes());

 // when:
  ResponseOptions response = given().spec(request)
    .put("/multipart");

 // then:
  assertThat(response.statusCode()).isEqualTo(200);
存根
            '''
{
  "request" : {
    "url" : "/multipart",
    "method" : "PUT",
    "headers" : {
      "Content-Type" : {
        "matches" : "multipart/form-data;boundary=AaB03x.*"
      }
    },
    "bodyPatterns" : [ {
        "matches" : ".*--(.*)\\r?\\nContent-Disposition: form-data; name=\\"formParameter\\"\\r?\\n(Content-Type: .*\\r?\\n)?(Content-Transfer-Encoding: .*\\r?\\n)?(Content-Length: \\\\d+\\r?\\n)?\\r?\\n\\".+\\"\\r?\\n--.*"
    }, {
        "matches" : ".*--(.*)\\r?\\nContent-Disposition: form-data; name=\\"someBooleanParameter\\"\\r?\\n(Content-Type: .*\\r?\\n)?(Content-Transfer-Encoding: .*\\r?\\n)?(Content-Length: \\\\d+\\r?\\n)?\\r?\\n(true|false)\\r?\\n--.*"
    }, {
      "matches" : ".*--(.*)\\r?\\nContent-Disposition: form-data; name=\\"file\\"; filename=\\"[\\\\S\\\\s]+\\"\\r?\\n(Content-Type: .*\\r?\\n)?(Content-Transfer-Encoding: .*\\r?\\n)?(Content-Length: \\\\d+\\r?\\n)?\\r?\\n[\\\\S\\\\s]+\\r?\\n--.*"
    } ]
  },
  "response" : {
    "status" : 200,
    "transformers" : [ "response-template", "foo-transformer" ]
  }
}
    '''

2.3. HTTP 响应

响应必须包含 HTTP 状态代码,并且可能包含其他信息。这 下面的代码显示了一个示例:spring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    request {
        //...
        method GET()
        url "/foo"
    }
    response {
        // Status code sent by the server
        // in response to request specified above.
        status OK()
    }
}
YAML
response:
...
status: 200
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
    c.request(r -> {
        // ...
        r.method(r.GET());
        r.url("/foo");
    });
    c.response(r -> {
        // Status code sent by the server
        // in response to request specified above.
        r.status(r.OK());
    });
});
Kotlin
contract {
    request {
        // ...
        method = GET
        url =url("/foo")
    }
    response {
        // Status code sent by the server
        // in response to request specified above.
        status = OK
    }
}

除了 status 之外,响应还可能包含 headers、cookie 和 body,它们是 指定方式与请求中相同(请参阅 HTTP 请求)。spring-doc.cn

在 Groovy DSL 中,您可以引用这些方法来提供有意义的状态,而不是数字。例如,您可以调用 status 或 .org.springframework.cloud.contract.spec.internal.HttpStatusOK()200BAD_REQUEST()400

2.4. 动态属性

合同可以包含一些动态属性:时间戳、ID 等。你不需要 想要强制 Consumer 对他们的 clockstub 进行存根,以始终返回相同的 time 值 以便它与存根匹配。spring-doc.cn

对于 Groovy DSL,您可以在 Contract 中提供动态部分 有两种方式:直接在正文中传递它们,或者将它们设置在名为 .bodyMatchersspring-doc.cn

在 2.0.0 之前,这些是使用 和 设置的。 有关更多信息,请参阅迁移指南testMatchersstubMatchers

对于 YAML,您只能使用 section.matchersspring-doc.cn

中的条目必须引用有效负载的现有元素。有关更多信息,请参阅此问题matchers

2.4.1. Body 内部的动态属性

此部分仅对编码的 DSL(Groovy、Java 等)有效。有关类似功能的 YAML 示例,请参阅 Matchers Sections 部分中的 Dynamic Properties

你可以使用 method 设置主体内部的属性,或者如果你使用 Groovy 映射表示法,其中 .以下示例演示如何设置 dynamic properties 替换为 value 方法:value$()spring-doc.cn

value(consumer(...), producer(...))
value(c(...), p(...))
value(stub(...), test(...))
value(client(...), server(...))
$
$(consumer(...), producer(...))
$(c(...), p(...))
$(stub(...), test(...))
$(client(...), server(...))

这两种方法同样有效。和 方法是方法的别名。后续部分将仔细研究您可以对这些值执行哪些操作。stubclientconsumerspring-doc.cn

2.4.2. 正则表达式

此部分仅对 Groovy DSL 有效。有关类似功能的 YAML 示例,请参阅 Matchers Sections 部分中的 Dynamic Properties

您可以使用正则表达式在合约 DSL 中写入您的请求。这样做是 当您想要指示应提供给定响应时,特别有用 对于遵循给定模式的请求。此外,在以下情况下,您可以使用正则表达式 需要对测试和服务器端测试使用模式而不是精确值。spring-doc.cn

确保 regex 匹配序列的整个区域,因为在内部调用 Pattern.matches() 。例如, 不匹配 ,但匹配。 还有一些其他已知限制abcaabc.abcspring-doc.cn

以下示例演示如何使用正则表达式编写请求:spring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    request {
        method('GET')
        url $(consumer(~/\/[0-9]{2}/), producer('/12'))
    }
    response {
        status OK()
        body(
                id: $(anyNumber()),
                surname: $(
                        consumer('Kowalsky'),
                        producer(regex('[a-zA-Z]+'))
                ),
                name: 'Jan',
                created: $(consumer('2014-02-02 12:23:43'), producer(execute('currentDate(it)'))),
                correlationId: value(consumer('5d1f9fef-e0dc-4f3d-a7e4-72d2220dd827'),
                        producer(regex('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}'))
                )
        )
        headers {
            header 'Content-Type': 'text/plain'
        }
    }
}
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
    c.request(r -> {
        r.method("GET");
        r.url(r.$(r.consumer(r.regex("\\/[0-9]{2}")), r.producer("/12")));
    });
    c.response(r -> {
        r.status(r.OK());
        r.body(ContractVerifierUtil.map().entry("id", r.$(r.anyNumber())).entry("surname",
                r.$(r.consumer("Kowalsky"), r.producer(r.regex("[a-zA-Z]+")))));
        r.headers(h -> {
            h.header("Content-Type", "text/plain");
        });
    });
});
Kotlin
contract {
    request {
        method = method("GET")
        url = url(v(consumer(regex("\\/[0-9]{2}")), producer("/12")))
    }
    response {
        status = OK
        body(mapOf(
                "id" to v(anyNumber),
                "surname" to v(consumer("Kowalsky"), producer(regex("[a-zA-Z]+")))
        ))
        headers {
            header("Content-Type", "text/plain")
        }
    }
}

您也可以使用正则表达式仅提供通信的一侧。如果你 执行此操作,则合约引擎会自动提供生成的匹配 提供的正则表达式。以下代码显示了 Groovy 的示例:spring-doc.cn

org.springframework.cloud.contract.spec.Contract.make {
    request {
        method 'PUT'
        url value(consumer(regex('/foo/[0-9]{5}')))
        body([
                requestElement: $(consumer(regex('[0-9]{5}')))
        ])
        headers {
            header('header', $(consumer(regex('application\\/vnd\\.fraud\\.v1\\+json;.*'))))
        }
    }
    response {
        status OK()
        body([
                responseElement: $(producer(regex('[0-9]{7}')))
        ])
        headers {
            contentType("application/vnd.fraud.v1+json")
        }
    }
}

在前面的示例中,通信的另一端具有相应的数据 为 request 和 response 生成。spring-doc.cn

Spring Cloud Contract 附带了一系列预定义的正则表达式,您可以 在你的 Contract 中使用,如下例所示:spring-doc.cn

public static RegexProperty onlyAlphaUnicode() {
    return new RegexProperty(ONLY_ALPHA_UNICODE).asString();
}

public static RegexProperty alphaNumeric() {
    return new RegexProperty(ALPHA_NUMERIC).asString();
}

public static RegexProperty number() {
    return new RegexProperty(NUMBER).asDouble();
}

public static RegexProperty positiveInt() {
    return new RegexProperty(POSITIVE_INT).asInteger();
}

public static RegexProperty anyBoolean() {
    return new RegexProperty(TRUE_OR_FALSE).asBooleanType();
}

public static RegexProperty anInteger() {
    return new RegexProperty(INTEGER).asInteger();
}

public static RegexProperty aDouble() {
    return new RegexProperty(DOUBLE).asDouble();
}

public static RegexProperty ipAddress() {
    return new RegexProperty(IP_ADDRESS).asString();
}

public static RegexProperty hostname() {
    return new RegexProperty(HOSTNAME_PATTERN).asString();
}

public static RegexProperty email() {
    return new RegexProperty(EMAIL).asString();
}

public static RegexProperty url() {
    return new RegexProperty(URL).asString();
}

public static RegexProperty httpsUrl() {
    return new RegexProperty(HTTPS_URL).asString();
}

public static RegexProperty uuid() {
    return new RegexProperty(UUID).asString();
}

public static RegexProperty uuid4() {
    return new RegexProperty(UUID4).asString();
}

public static RegexProperty isoDate() {
    return new RegexProperty(ANY_DATE).asString();
}

public static RegexProperty isoDateTime() {
    return new RegexProperty(ANY_DATE_TIME).asString();
}

public static RegexProperty isoTime() {
    return new RegexProperty(ANY_TIME).asString();
}

public static RegexProperty iso8601WithOffset() {
    return new RegexProperty(ISO8601_WITH_OFFSET).asString();
}

public static RegexProperty nonEmpty() {
    return new RegexProperty(NON_EMPTY).asString();
}

public static RegexProperty nonBlank() {
    return new RegexProperty(NON_BLANK).asString();
}

在你的 Contract 中,你可以按如下方式使用它(Groovy DSL 的示例):spring-doc.cn

Contract dslWithOptionalsInString = Contract.make {
    priority 1
    request {
        method POST()
        url '/users/password'
        headers {
            contentType(applicationJson())
        }
        body(
                email: $(consumer(optional(regex(email()))), producer('[email protected]')),
                callback_url: $(consumer(regex(hostname())), producer('http://partners.com'))
        )
    }
    response {
        status 404
        headers {
            contentType(applicationJson())
        }
        body(
                code: value(consumer("123123"), producer(optional("123123"))),
                message: "User not found by email = [${value(producer(regex(email())), consumer('[email protected]'))}]"
        )
    }
}

为了让事情变得更简单,您可以使用一组预定义的对象,这些对象会自动 假设您希望传递正则表达式。 所有这些方法都以 prefix 开头,如下所示:anyspring-doc.cn

T anyAlphaUnicode();

T anyAlphaNumeric();

T anyNumber();

T anyInteger();

T anyPositiveInt();

T anyDouble();

T anyHex();

T aBoolean();

T anyIpAddress();

T anyHostname();

T anyEmail();

T anyUrl();

T anyHttpsUrl();

T anyUuid();

T anyDate();

T anyDateTime();

T anyTime();

T anyIso8601WithOffset();

T anyNonBlankString();

T anyNonEmptyString();

T anyOf(String... values);

下面的示例演示如何引用这些方法:spring-doc.cn

槽的
Contract contractDsl = Contract.make {
    name "foo"
    label 'trigger_event'
    input {
        triggeredBy('toString()')
    }
    outputMessage {
        sentTo 'topic.rateablequote'
        body([
                alpha            : $(anyAlphaUnicode()),
                number           : $(anyNumber()),
                anInteger        : $(anyInteger()),
                positiveInt      : $(anyPositiveInt()),
                aDouble          : $(anyDouble()),
                aBoolean         : $(aBoolean()),
                ip               : $(anyIpAddress()),
                hostname         : $(anyHostname()),
                email            : $(anyEmail()),
                url              : $(anyUrl()),
                httpsUrl         : $(anyHttpsUrl()),
                uuid             : $(anyUuid()),
                date             : $(anyDate()),
                dateTime         : $(anyDateTime()),
                time             : $(anyTime()),
                iso8601WithOffset: $(anyIso8601WithOffset()),
                nonBlankString   : $(anyNonBlankString()),
                nonEmptyString   : $(anyNonEmptyString()),
                anyOf            : $(anyOf('foo', 'bar'))
        ])
    }
}
Kotlin
contract {
    name = "foo"
    label = "trigger_event"
    input {
        triggeredBy = "toString()"
    }
    outputMessage {
        sentTo = sentTo("topic.rateablequote")
        body(mapOf(
                "alpha" to v(anyAlphaUnicode),
                "number" to v(anyNumber),
                "anInteger" to v(anyInteger),
                "positiveInt" to v(anyPositiveInt),
                "aDouble" to v(anyDouble),
                "aBoolean" to v(aBoolean),
                "ip" to v(anyIpAddress),
                "hostname" to v(anyAlphaUnicode),
                "email" to v(anyEmail),
                "url" to v(anyUrl),
                "httpsUrl" to v(anyHttpsUrl),
                "uuid" to v(anyUuid),
                "date" to v(anyDate),
                "dateTime" to v(anyDateTime),
                "time" to v(anyTime),
                "iso8601WithOffset" to v(anyIso8601WithOffset),
                "nonBlankString" to v(anyNonBlankString),
                "nonEmptyString" to v(anyNonEmptyString),
                "anyOf" to v(anyOf('foo', 'bar'))
        ))
        headers {
            header("Content-Type", "text/plain")
        }
    }
}
局限性
由于库的某些限制,从 一个正则表达式,如果您依赖 Automatic,请不要在正则表达式中使用 和 符号 代。参见 问题 899Xeger$^
请勿将实例用作 (例如) 的值。 它会导致 .请改用。 参见 问题 900LocalDate$$(consumer(LocalDate.now()))java.lang.StackOverflowError$(consumer(LocalDate.now().toString()))

2.4.3. 传递可选参数

此部分仅对 Groovy DSL 有效。有关类似功能的 YAML 示例,请参阅 Matchers Sections 部分中的 Dynamic Properties

您可以在合同中提供可选参数。但是,您可以提供 可选参数仅适用于以下各项:spring-doc.cn

以下示例说明如何提供可选参数:spring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    priority 1
    name "optionals"
    request {
        method 'POST'
        url '/users/password'
        headers {
            contentType(applicationJson())
        }
        body(
                email: $(consumer(optional(regex(email()))), producer('[email protected]')),
                callback_url: $(consumer(regex(hostname())), producer('https://partners.com'))
        )
    }
    response {
        status 404
        headers {
            header 'Content-Type': 'application/json'
        }
        body(
                code: value(consumer("123123"), producer(optional("123123")))
        )
    }
}
Java
org.springframework.cloud.contract.spec.Contract.make(c -> {
    c.priority(1);
    c.name("optionals");
    c.request(r -> {
        r.method("POST");
        r.url("/users/password");
        r.headers(h -> {
            h.contentType(h.applicationJson());
        });
        r.body(ContractVerifierUtil.map()
                .entry("email", r.$(r.consumer(r.optional(r.regex(r.email()))), r.producer("[email protected]")))
                .entry("callback_url",
                        r.$(r.consumer(r.regex(r.hostname())), r.producer("https://partners.com"))));
    });
    c.response(r -> {
        r.status(404);
        r.headers(h -> {
            h.header("Content-Type", "application/json");
        });
        r.body(ContractVerifierUtil.map().entry("code",
                r.value(r.consumer("123123"), r.producer(r.optional("123123")))));
    });
});
Kotlin
contract { c ->
    priority = 1
    name = "optionals"
    request {
        method = POST
        url = url("/users/password")
        headers {
            contentType = APPLICATION_JSON
        }
        body = body(mapOf(
                "email" to v(consumer(optional(regex(email))), producer("[email protected]")),
                "callback_url" to v(consumer(regex(hostname)), producer("https://partners.com"))
        ))
    }
    response {
        status = NOT_FOUND
        headers {
            header("Content-Type", "application/json")
        }
        body(mapOf(
                "code" to value(consumer("123123"), producer(optional("123123")))
        ))
    }
}

通过使用该方法包装 body 的一部分,您可以创建一个常规的 expression 的表达式,该表达式必须出现 0 次或多次。optional()spring-doc.cn

如果您使用 Spock,则将根据前面的示例生成以下测试:spring-doc.cn

槽的
package com.example

import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import spock.lang.Specification
import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification
import io.restassured.response.ResponseOptions

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson
import static io.restassured.module.mockmvc.RestAssuredMockMvc.*

class FooSpec extends Specification {

\tdef validate_optionals() throws Exception {
\t\tgiven:
\t\t\tMockMvcRequestSpecification request = given()
\t\t\t\t\t.header("Content-Type", "application/json")
\t\t\t\t\t.body('''{"email":"[email protected]","callback_url":"https://partners.com"}''')

\t\twhen:
\t\t\tResponseOptions response = given().spec(request)
\t\t\t\t\t.post("/users/password")

\t\tthen:
\t\t\tresponse.statusCode() == 404
\t\t\tresponse.header("Content-Type") == 'application/json'

\t\tand:
\t\t\tDocumentContext parsedJson = JsonPath.parse(response.body.asString())
\t\t\tassertThatJson(parsedJson).field("['code']").matches("(123123)?")
\t}

}

还将生成以下存根:spring-doc.cn

                    '''
{
  "request" : {
    "url" : "/users/password",
    "method" : "POST",
    "bodyPatterns" : [ {
      "matchesJsonPath" : "$[?(@.['email'] =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,6})?/)]"
    }, {
      "matchesJsonPath" : "$[?(@.['callback_url'] =~ /((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?/)]"
    } ],
    "headers" : {
      "Content-Type" : {
        "equalTo" : "application/json"
      }
    }
  },
  "response" : {
    "status" : 404,
    "body" : "{\\"code\\":\\"123123\\",\\"message\\":\\"User not found by email == [[email protected]]\\"}",
    "headers" : {
      "Content-Type" : "application/json"
    }
  },
  "priority" : 1
}
'''

2.4.4. 在服务器端调用自定义方法

此部分仅对 Groovy DSL 有效。有关类似功能的 YAML 示例,请参阅 Matchers Sections 部分中的 Dynamic Properties

您可以定义在测试期间在服务器端运行的方法调用。这样的 方法可以添加到配置中定义的类中。这 以下代码显示了测试用例的 Contract 部分的示例:baseClassForTestsspring-doc.cn

槽的
method GET()
Java
r.method(r.GET());
Kotlin
method = GET

下面的代码显示了测试用例的基类部分:spring-doc.cn

abstract class BaseMockMvcSpec extends Specification {

    def setup() {
        RestAssuredMockMvc.standaloneSetup(new PairIdController())
    }

    void isProperCorrelationId(Integer correlationId) {
        assert correlationId == 123456
    }

    void isEmpty(String value) {
        assert value == null
    }

}
不能同时使用 a 和 来执行串联。为 示例,调用潜在客户 不正确的结果。相反,调用 和 确保该方法返回您需要的所有内容。Stringexecuteheader('Authorization', 'Bearer ' + execute('authToken()'))header('Authorization', execute('authToken()'))authToken()

从 JSON 中读取的对象类型可以是以下类型之一,具体取决于 JSON 路径:spring-doc.cn

  • String:如果您指向 JSON 中的某个值。Stringspring-doc.cn

  • JSONArray:如果您指向 JSON 中的 a。Listspring-doc.cn

  • Map:如果您指向 JSON 中的 a。Mapspring-doc.cn

  • Number:如果您指向 、 和 JSON 中的其他数字类型。IntegerDoublespring-doc.cn

  • Boolean:如果您指向 JSON 中的 a。Booleanspring-doc.cn

在合同的 request 部分,您可以指定 should take from 方法。bodyspring-doc.cn

您必须同时提供使用者端和生产者端。零件 应用于整个身体,而不是身体的某些部分。execute

以下示例显示如何从 JSON 中读取对象:spring-doc.cn

Contract contractDsl = Contract.make {
    request {
        method 'GET'
        url '/something'
        body(
                $(c('foo'), p(execute('hashCode()')))
        )
    }
    response {
        status OK()
    }
}

前面的示例导致在请求正文中调用该方法。 它应类似于以下代码:hashCode()spring-doc.cn

// given:
 MockMvcRequestSpecification request = given()
   .body(hashCode());

// when:
 ResponseOptions response = given().spec(request)
   .get("/something");

// then:
 assertThat(response.statusCode()).isEqualTo(200);

2.4.5. 从响应中引用 Request

最好的情况是提供固定值,但有时您需要引用 request 的响应。spring-doc.cn

如果你在 Groovy DSL 中编写契约,你可以使用该方法,它允许 您从 HTTP 请求中引用了一组元素。您可以使用以下内容 选项:fromRequest()spring-doc.cn

  • fromRequest().url():返回请求 URL 和查询参数。spring-doc.cn

  • fromRequest().query(String key):返回具有给定名称的第一个查询参数。spring-doc.cn

  • fromRequest().query(String key, int index):返回第 n 个查询参数,其中 名。spring-doc.cn

  • fromRequest().path():返回完整路径。spring-doc.cn

  • fromRequest().path(int index):返回第 n 个 path 元素。spring-doc.cn

  • fromRequest().header(String key):返回具有给定名称的第一个标头。spring-doc.cn

  • fromRequest().header(String key, int index):返回具有给定名称的第 n 个标头。spring-doc.cn

  • fromRequest().body():返回完整的请求正文。spring-doc.cn

  • fromRequest().body(String jsonPath):从请求中返回 匹配 JSON 路径。spring-doc.cn

如果使用 YAML Contract 定义或 Java Contract 定义,则必须将 Handlebars 表示法与自定义 Spring Cloud Contract 一起使用 功能来实现此目的。在这种情况下,您可以使用以下选项:{{{ }}}spring-doc.cn

  • {{{ request.url }}}:返回请求 URL 和查询参数。spring-doc.cn

  • {{{ request.query.key.[index] }}}:返回具有给定名称的第 n 个查询参数。 例如,对于 键 ,第一个条目是thing{{{ request.query.thing.[0] }}}spring-doc.cn

  • {{{ request.path }}}:返回完整路径。spring-doc.cn

  • {{{ request.path.[index] }}}:返回第 n 个 path 元素。例如 第一个条目是 {{{ request.path.[0] }}}`spring-doc.cn

  • {{{ request.headers.key }}}:返回具有给定名称的第一个标头。spring-doc.cn

  • {{{ request.headers.key.[index] }}}:返回具有给定名称的第 n 个标头。spring-doc.cn

  • {{{ request.body }}}:返回完整的请求正文。spring-doc.cn

  • {{{ jsonpath this 'your.json.path' }}}:从请求中返回 匹配 JSON 路径。例如,对于 JSON 路径 ,请使用$.here{{{ jsonpath this '$.here' }}}spring-doc.cn

考虑以下合约:spring-doc.cn

槽的
Contract contractDsl = Contract.make {
    request {
        method 'GET'
        url('/api/v1/xxxx') {
            queryParameters {
                parameter('foo', 'bar')
                parameter('foo', 'bar2')
            }
        }
        headers {
            header(authorization(), 'secret')
            header(authorization(), 'secret2')
        }
        body(foo: 'bar', baz: 5)
    }
    response {
        status OK()
        headers {
            header(authorization(), "foo ${fromRequest().header(authorization())} bar")
        }
        body(
                url: fromRequest().url(),
                path: fromRequest().path(),
                pathIndex: fromRequest().path(1),
                param: fromRequest().query('foo'),
                paramIndex: fromRequest().query('foo', 1),
                authorization: fromRequest().header('Authorization'),
                authorization2: fromRequest().header('Authorization', 1),
                fullBody: fromRequest().body(),
                responseFoo: fromRequest().body('$.foo'),
                responseBaz: fromRequest().body('$.baz'),
                responseBaz2: "Bla bla ${fromRequest().body('$.foo')} bla bla",
                rawUrl: fromRequest().rawUrl(),
                rawPath: fromRequest().rawPath(),
                rawPathIndex: fromRequest().rawPath(1),
                rawParam: fromRequest().rawQuery('foo'),
                rawParamIndex: fromRequest().rawQuery('foo', 1),
                rawAuthorization: fromRequest().rawHeader('Authorization'),
                rawAuthorization2: fromRequest().rawHeader('Authorization', 1),
                rawResponseFoo: fromRequest().rawBody('$.foo'),
                rawResponseBaz: fromRequest().rawBody('$.baz'),
                rawResponseBaz2: "Bla bla ${fromRequest().rawBody('$.foo')} bla bla"
        )
    }
}
Contract contractDsl = Contract.make {
    request {
        method 'GET'
        url('/api/v1/xxxx') {
            queryParameters {
                parameter('foo', 'bar')
                parameter('foo', 'bar2')
            }
        }
        headers {
            header(authorization(), 'secret')
            header(authorization(), 'secret2')
        }
        body(foo: "bar", baz: 5)
    }
    response {
        status OK()
        headers {
            contentType(applicationJson())
        }
        body('''
                {
                    "responseFoo": "{{{ jsonPath request.body '$.foo' }}}",
                    "responseBaz": {{{ jsonPath request.body '$.baz' }}},
                    "responseBaz2": "Bla bla {{{ jsonPath request.body '$.foo' }}} bla bla"
                }
        '''.toString())
    }
}
YAML
request:
  method: GET
  url: /api/v1/xxxx
  queryParameters:
    foo:
      - bar
      - bar2
  headers:
    Authorization:
      - secret
      - secret2
  body:
    foo: bar
    baz: 5
response:
  status: 200
  headers:
    Authorization: "foo {{{ request.headers.Authorization.0 }}} bar"
  body:
    url: "{{{ request.url }}}"
    path: "{{{ request.path }}}"
    pathIndex: "{{{ request.path.1 }}}"
    param: "{{{ request.query.foo }}}"
    paramIndex: "{{{ request.query.foo.1 }}}"
    authorization: "{{{ request.headers.Authorization.0 }}}"
    authorization2: "{{{ request.headers.Authorization.1 }}"
    fullBody: "{{{ request.body }}}"
    responseFoo: "{{{ jsonpath this '$.foo' }}}"
    responseBaz: "{{{ jsonpath this '$.baz' }}}"
    responseBaz2: "Bla bla {{{ jsonpath this '$.foo' }}} bla bla"
Java
package contracts.beer.rest;

import java.util.function.Supplier;

import org.springframework.cloud.contract.spec.Contract;

import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.map;

class shouldReturnStatsForAUser implements Supplier<Contract> {

    @Override
    public Contract get() {
        return Contract.make(c -> {
            c.request(r -> {
                r.method("POST");
                r.url("/stats");
                r.body(map().entry("name", r.anyAlphaUnicode()));
                r.headers(h -> {
                    h.contentType(h.applicationJson());
                });
            });
            c.response(r -> {
                r.status(r.OK());
                r.body(map()
                        .entry("text",
                                "Dear {{{jsonPath request.body '$.name'}}} thanks for your interested in drinking beer")
                        .entry("quantity", r.$(r.c(5), r.p(r.anyNumber()))));
                r.headers(h -> {
                    h.contentType(h.applicationJson());
                });
            });
        });
    }

}
Kotlin
package contracts.beer.rest

import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

contract {
    request {
        method = method("POST")
        url = url("/stats")
        body(mapOf(
            "name" to anyAlphaUnicode
        ))
        headers {
            contentType = APPLICATION_JSON
        }
    }
    response {
        status = OK
        body(mapOf(
            "text" to "Don't worry ${fromRequest().body("$.name")} thanks for your interested in drinking beer",
            "quantity" to v(c(5), p(anyNumber))
        ))
        headers {
            contentType = fromRequest().header(CONTENT_TYPE)
        }
    }
}

运行 JUnit 测试生成会导致类似于以下示例的测试:spring-doc.cn

// given:
 MockMvcRequestSpecification request = given()
   .header("Authorization", "secret")
   .header("Authorization", "secret2")
   .body("{\"foo\":\"bar\",\"baz\":5}");

// when:
 ResponseOptions response = given().spec(request)
   .queryParam("foo","bar")
   .queryParam("foo","bar2")
   .get("/api/v1/xxxx");

// then:
 assertThat(response.statusCode()).isEqualTo(200);
 assertThat(response.header("Authorization")).isEqualTo("foo secret bar");
// and:
 DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
 assertThatJson(parsedJson).field("['fullBody']").isEqualTo("{\"foo\":\"bar\",\"baz\":5}");
 assertThatJson(parsedJson).field("['authorization']").isEqualTo("secret");
 assertThatJson(parsedJson).field("['authorization2']").isEqualTo("secret2");
 assertThatJson(parsedJson).field("['path']").isEqualTo("/api/v1/xxxx");
 assertThatJson(parsedJson).field("['param']").isEqualTo("bar");
 assertThatJson(parsedJson).field("['paramIndex']").isEqualTo("bar2");
 assertThatJson(parsedJson).field("['pathIndex']").isEqualTo("v1");
 assertThatJson(parsedJson).field("['responseBaz']").isEqualTo(5);
 assertThatJson(parsedJson).field("['responseFoo']").isEqualTo("bar");
 assertThatJson(parsedJson).field("['url']").isEqualTo("/api/v1/xxxx?foo=bar&foo=bar2");
 assertThatJson(parsedJson).field("['responseBaz2']").isEqualTo("Bla bla bar bla bla");

如您所见,请求中的元素已在响应中正确引用。spring-doc.cn

生成的 WireMock 存根应类似于以下示例:spring-doc.cn

{
  "request" : {
    "urlPath" : "/api/v1/xxxx",
    "method" : "POST",
    "headers" : {
      "Authorization" : {
        "equalTo" : "secret2"
      }
    },
    "queryParameters" : {
      "foo" : {
        "equalTo" : "bar2"
      }
    },
    "bodyPatterns" : [ {
      "matchesJsonPath" : "$[?(@.['baz'] == 5)]"
    }, {
      "matchesJsonPath" : "$[?(@.['foo'] == 'bar')]"
    } ]
  },
  "response" : {
    "status" : 200,
    "body" : "{\"authorization\":\"{{{request.headers.Authorization.[0]}}}\",\"path\":\"{{{request.path}}}\",\"responseBaz\":{{{jsonpath this '$.baz'}}} ,\"param\":\"{{{request.query.foo.[0]}}}\",\"pathIndex\":\"{{{request.path.[1]}}}\",\"responseBaz2\":\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\",\"responseFoo\":\"{{{jsonpath this '$.foo'}}}\",\"authorization2\":\"{{{request.headers.Authorization.[1]}}}\",\"fullBody\":\"{{{escapejsonbody}}}\",\"url\":\"{{{request.url}}}\",\"paramIndex\":\"{{{request.query.foo.[1]}}}\"}",
    "headers" : {
      "Authorization" : "{{{request.headers.Authorization.[0]}}};foo"
    },
    "transformers" : [ "response-template" ]
  }
}

发送请求,例如合同结果部分中显示的请求 在发送以下响应正文时:requestspring-doc.cn

{
  "url" : "/api/v1/xxxx?foo=bar&foo=bar2",
  "path" : "/api/v1/xxxx",
  "pathIndex" : "v1",
  "param" : "bar",
  "paramIndex" : "bar2",
  "authorization" : "secret",
  "authorization2" : "secret2",
  "fullBody" : "{\"foo\":\"bar\",\"baz\":5}",
  "responseFoo" : "bar",
  "responseBaz" : 5,
  "responseBaz2" : "Bla bla bar bla bla"
}
此功能仅适用于大于或等于 WireMock 的版本 更改为 2.5.1。Spring Cloud Contract Verifier 使用 WireMock 的响应转换器。它使用 Handlebars 将 Mustache 模板转换为 适当的值。此外,它还注册了两个帮助程序函数:response-template{{{ }}}
  • escapejsonbody:以可嵌入 JSON 的格式转义请求正文。spring-doc.cn

  • jsonpath:对于给定的参数,在请求正文中查找对象。spring-doc.cn

2.4.6. Matchers 部分中的动态属性

如果您使用 Pact,以下讨论可能看起来很熟悉。 相当多的用户习惯于在 body 和设置 合同的动态部分。spring-doc.cn

您可以使用该部分有两个原因:bodyMatchersspring-doc.cn

  • 定义应以存根结尾的动态值。 您可以在 Contract 的 或 部分 中设置它。requestinputMessagespring-doc.cn

  • 验证您的测试结果。 此部分位于 或 合同。responseoutputMessagespring-doc.cn

目前, Spring Cloud Contract Verifier 仅支持基于 JSON 路径的匹配器,其中 以下匹配可能性:spring-doc.cn

编码 DSL

对于 stub (在使用者端的测试中):spring-doc.cn

  • byEquality():从提供的 JSON 路径中的使用者请求中获取的值必须为 等于合同中提供的值。spring-doc.cn

  • byRegex(…​):从提供的 JSON 路径中的使用者请求中获取的值必须 匹配正则表达式。您还可以传递预期匹配值的类型(例如, , , 等)。asString()asLong()spring-doc.cn

  • byDate():从提供的 JSON 路径中的使用者请求中获取的值必须 匹配 ISO 日期值的正则表达式。spring-doc.cn

  • byTimestamp():从提供的 JSON 路径中的使用者请求中获取的值必须 匹配 ISO DateTime 值的正则表达式。spring-doc.cn

  • byTime():从提供的 JSON 路径中的使用者请求中获取的值必须 匹配 ISO Time 值的正则表达式。spring-doc.cn

对于验证(在 Producer 端生成的测试中):spring-doc.cn

  • byEquality():从提供的 JSON 路径中的生产者响应中获取的值必须为 等于 Contract 中提供的值。spring-doc.cn

  • byRegex(…​):从提供的 JSON 路径中的生产者响应中获取的值必须 匹配正则表达式。spring-doc.cn

  • byDate():从提供的 JSON 路径中的生产者响应中获取的值必须匹配 ISO Date 值的正则表达式。spring-doc.cn

  • byTimestamp():从提供的 JSON 路径中的生产者响应中获取的值必须 匹配 ISO DateTime 值的正则表达式。spring-doc.cn

  • byTime():从提供的 JSON 路径中的生产者响应中获取的值必须匹配 ISO Time 值的正则表达式。spring-doc.cn

  • byType():从提供的 JSON 路径中的生产者响应中获取的值需要为 与协定中响应正文中定义的类型相同。 可以采用一个闭包,您可以在其中设置 和 。对于 请求端,你应该使用 closure 来断言集合的大小。 这样,您就可以断言扁平化集合的大小。要检查 unflattened 集合中,请使用 .byTypeminOccurrencemaxOccurrencebyCommand(…​)testMatcherspring-doc.cn

  • byCommand(…​):从提供的 JSON 路径中的生产者响应中获取的值为 作为输入传递给您提供的自定义方法。例如,导致调用一个方法,其中与 JSON 路径被传递。从 JSON 中读取的对象类型可以是 ,具体取决于 JSON 路径:byCommand('thing($it)')thingspring-doc.cn

  • byNull():从提供的 JSON 路径中的响应中获取的值必须为 null。spring-doc.cn

YAML
有关 类型含义。

对于 YAML,匹配器的结构类似于以下示例:spring-doc.cn

- path: $.thing1
  type: by_regex
  value: thing2
  regexType: as_string

或者,如果要使用预定义的正则表达式之一,可以使用类似于以下示例的内容:[only_alpha_unicode, number, any_boolean, ip_address, hostname, email, url, uuid, iso_date, iso_date_time, iso_time, iso_8601_with_offset, non_empty, non_blank]spring-doc.cn

- path: $.thing1
  type: by_regex
  predefined: only_alpha_unicode

以下列表显示了允许的值列表:typespring-doc.cn

您还可以定义正则表达式在字段中对应的类型。以下列表显示了允许的正则表达式类型:regexTypespring-doc.cn

请考虑以下示例:spring-doc.cn

槽的
Contract contractDsl = Contract.make {
    request {
        method 'GET'
        urlPath '/get'
        body([
                duck                : 123,
                alpha               : 'abc',
                number              : 123,
                aBoolean            : true,
                date                : '2017-01-01',
                dateTime            : '2017-01-01T01:23:45',
                time                : '01:02:34',
                valueWithoutAMatcher: 'foo',
                valueWithTypeMatch  : 'string',
                key                 : [
                        'complex.key': 'foo'
                ]
        ])
        bodyMatchers {
            jsonPath('$.duck', byRegex("[0-9]{3}").asInteger())
            jsonPath('$.duck', byEquality())
            jsonPath('$.alpha', byRegex(onlyAlphaUnicode()).asString())
            jsonPath('$.alpha', byEquality())
            jsonPath('$.number', byRegex(number()).asInteger())
            jsonPath('$.aBoolean', byRegex(anyBoolean()).asBooleanType())
            jsonPath('$.date', byDate())
            jsonPath('$.dateTime', byTimestamp())
            jsonPath('$.time', byTime())
            jsonPath("\$.['key'].['complex.key']", byEquality())
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status OK()
        body([
                duck                 : 123,
                alpha                : 'abc',
                number               : 123,
                positiveInteger      : 1234567890,
                negativeInteger      : -1234567890,
                positiveDecimalNumber: 123.4567890,
                negativeDecimalNumber: -123.4567890,
                aBoolean             : true,
                date                 : '2017-01-01',
                dateTime             : '2017-01-01T01:23:45',
                time                 : "01:02:34",
                valueWithoutAMatcher : 'foo',
                valueWithTypeMatch   : 'string',
                valueWithMin         : [
                        1, 2, 3
                ],
                valueWithMax         : [
                        1, 2, 3
                ],
                valueWithMinMax      : [
                        1, 2, 3
                ],
                valueWithMinEmpty    : [],
                valueWithMaxEmpty    : [],
                key                  : [
                        'complex.key': 'foo'
                ],
                nullValue            : null
        ])
        bodyMatchers {
            // asserts the jsonpath value against manual regex
            jsonPath('$.duck', byRegex("[0-9]{3}").asInteger())
            // asserts the jsonpath value against the provided value
            jsonPath('$.duck', byEquality())
            // asserts the jsonpath value against some default regex
            jsonPath('$.alpha', byRegex(onlyAlphaUnicode()).asString())
            jsonPath('$.alpha', byEquality())
            jsonPath('$.number', byRegex(number()).asInteger())
            jsonPath('$.positiveInteger', byRegex(anInteger()).asInteger())
            jsonPath('$.negativeInteger', byRegex(anInteger()).asInteger())
            jsonPath('$.positiveDecimalNumber', byRegex(aDouble()).asDouble())
            jsonPath('$.negativeDecimalNumber', byRegex(aDouble()).asDouble())
            jsonPath('$.aBoolean', byRegex(anyBoolean()).asBooleanType())
            // asserts vs inbuilt time related regex
            jsonPath('$.date', byDate())
            jsonPath('$.dateTime', byTimestamp())
            jsonPath('$.time', byTime())
            // asserts that the resulting type is the same as in response body
            jsonPath('$.valueWithTypeMatch', byType())
            jsonPath('$.valueWithMin', byType {
                // results in verification of size of array (min 1)
                minOccurrence(1)
            })
            jsonPath('$.valueWithMax', byType {
                // results in verification of size of array (max 3)
                maxOccurrence(3)
            })
            jsonPath('$.valueWithMinMax', byType {
                // results in verification of size of array (min 1 & max 3)
                minOccurrence(1)
                maxOccurrence(3)
            })
            jsonPath('$.valueWithMinEmpty', byType {
                // results in verification of size of array (min 0)
                minOccurrence(0)
            })
            jsonPath('$.valueWithMaxEmpty', byType {
                // results in verification of size of array (max 0)
                maxOccurrence(0)
            })
            // will execute a method `assertThatValueIsANumber`
            jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)'))
            jsonPath("\$.['key'].['complex.key']", byEquality())
            jsonPath('$.nullValue', byNull())
        }
        headers {
            contentType(applicationJson())
            header('Some-Header', $(c('someValue'), p(regex('[a-zA-Z]{9}'))))
        }
    }
}
YAML
request:
  method: GET
  urlPath: /get/1
  headers:
    Content-Type: application/json
  cookies:
    foo: 2
    bar: 3
  queryParameters:
    limit: 10
    offset: 20
    filter: 'email'
    sort: name
    search: 55
    age: 99
    name: John.Doe
    email: '[email protected]'
  body:
    duck: 123
    alpha: "abc"
    number: 123
    aBoolean: true
    date: "2017-01-01"
    dateTime: "2017-01-01T01:23:45"
    time: "01:02:34"
    valueWithoutAMatcher: "foo"
    valueWithTypeMatch: "string"
    key:
      "complex.key": 'foo'
    nullValue: null
    valueWithMin:
      - 1
      - 2
      - 3
    valueWithMax:
      - 1
      - 2
      - 3
    valueWithMinMax:
      - 1
      - 2
      - 3
    valueWithMinEmpty: []
    valueWithMaxEmpty: []
  matchers:
    url:
      regex: /get/[0-9]
      # predefined:
      # execute a method
      #command: 'equals($it)'
    queryParameters:
      - key: limit
        type: equal_to
        value: 20
      - key: offset
        type: containing
        value: 20
      - key: sort
        type: equal_to
        value: name
      - key: search
        type: not_matching
        value: '^[0-9]{2}$'
      - key: age
        type: not_matching
        value: '^\\w*$'
      - key: name
        type: matching
        value: 'John.*'
      - key: hello
        type: absent
    cookies:
      - key: foo
        regex: '[0-9]'
      - key: bar
        command: 'equals($it)'
    headers:
      - key: Content-Type
        regex: "application/json.*"
    body:
      - path: $.duck
        type: by_regex
        value: "[0-9]{3}"
      - path: $.duck
        type: by_equality
      - path: $.alpha
        type: by_regex
        predefined: only_alpha_unicode
      - path: $.alpha
        type: by_equality
      - path: $.number
        type: by_regex
        predefined: number
      - path: $.aBoolean
        type: by_regex
        predefined: any_boolean
      - path: $.date
        type: by_date
      - path: $.dateTime
        type: by_timestamp
      - path: $.time
        type: by_time
      - path: "$.['key'].['complex.key']"
        type: by_equality
      - path: $.nullvalue
        type: by_null
      - path: $.valueWithMin
        type: by_type
        minOccurrence: 1
      - path: $.valueWithMax
        type: by_type
        maxOccurrence: 3
      - path: $.valueWithMinMax
        type: by_type
        minOccurrence: 1
        maxOccurrence: 3
response:
  status: 200
  cookies:
    foo: 1
    bar: 2
  body:
    duck: 123
    alpha: "abc"
    number: 123
    aBoolean: true
    date: "2017-01-01"
    dateTime: "2017-01-01T01:23:45"
    time: "01:02:34"
    valueWithoutAMatcher: "foo"
    valueWithTypeMatch: "string"
    valueWithMin:
      - 1
      - 2
      - 3
    valueWithMax:
      - 1
      - 2
      - 3
    valueWithMinMax:
      - 1
      - 2
      - 3
    valueWithMinEmpty: []
    valueWithMaxEmpty: []
    key:
      'complex.key': 'foo'
    nulValue: null
  matchers:
    headers:
      - key: Content-Type
        regex: "application/json.*"
    cookies:
      - key: foo
        regex: '[0-9]'
      - key: bar
        command: 'equals($it)'
    body:
      - path: $.duck
        type: by_regex
        value: "[0-9]{3}"
      - path: $.duck
        type: by_equality
      - path: $.alpha
        type: by_regex
        predefined: only_alpha_unicode
      - path: $.alpha
        type: by_equality
      - path: $.number
        type: by_regex
        predefined: number
      - path: $.aBoolean
        type: by_regex
        predefined: any_boolean
      - path: $.date
        type: by_date
      - path: $.dateTime
        type: by_timestamp
      - path: $.time
        type: by_time
      - path: $.valueWithTypeMatch
        type: by_type
      - path: $.valueWithMin
        type: by_type
        minOccurrence: 1
      - path: $.valueWithMax
        type: by_type
        maxOccurrence: 3
      - path: $.valueWithMinMax
        type: by_type
        minOccurrence: 1
        maxOccurrence: 3
      - path: $.valueWithMinEmpty
        type: by_type
        minOccurrence: 0
      - path: $.valueWithMaxEmpty
        type: by_type
        maxOccurrence: 0
      - path: $.duck
        type: by_command
        value: assertThatValueIsANumber($it)
      - path: $.nullValue
        type: by_null
        value: null
  headers:
    Content-Type: application/json

在前面的示例中,您可以在各节中看到协定的动态部分。对于请求部分,您可以看到,对于除 之外的所有字段,存根应 contain 的 intent 是对于 ,将进行验证 与不使用 matchers 的方式相同。在这种情况下,测试会执行 相等性检查。matchersvalueWithoutAMatchervalueWithoutAMatcherspring-doc.cn

对于本节中的响应端,我们在 类似的方式。唯一的区别是 matchers 也存在。这 验证程序引擎检查四个字段,以验证来自测试的响应 的值 JSON 路径与给定字段匹配,与该值的类型相同 定义,并通过以下检查(基于正在调用的方法):bodyMatchersbyTypespring-doc.cn

  • 对于 ,引擎检查类型是否相同。$.valueWithTypeMatchspring-doc.cn

  • 对于 ,引擎会检查类型并断言大小是否更大 小于或等于最小出现次数。$.valueWithMinspring-doc.cn

  • 对于 ,引擎会检查类型并断言大小是否为 小于或等于最大出现次数。$.valueWithMaxspring-doc.cn

  • 对于 ,引擎会检查类型并断言大小是否为 介于最小出现次数和最大出现次数之间。$.valueWithMinMaxspring-doc.cn

生成的测试类似于以下示例(请注意,部分 将自动生成的断言和断言与 matchers 分开):andspring-doc.cn

// given:
 MockMvcRequestSpecification request = given()
   .header("Content-Type", "application/json")
   .body("{\"duck\":123,\"alpha\":\"abc\",\"number\":123,\"aBoolean\":true,\"date\":\"2017-01-01\",\"dateTime\":\"2017-01-01T01:23:45\",\"time\":\"01:02:34\",\"valueWithoutAMatcher\":\"foo\",\"valueWithTypeMatch\":\"string\",\"key\":{\"complex.key\":\"foo\"}}");

// when:
 ResponseOptions response = given().spec(request)
   .get("/get");

// then:
 assertThat(response.statusCode()).isEqualTo(200);
 assertThat(response.header("Content-Type")).matches("application/json.*");
// and:
 DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
 assertThatJson(parsedJson).field("['valueWithoutAMatcher']").isEqualTo("foo");
// and:
 assertThat(parsedJson.read("$.duck", String.class)).matches("[0-9]{3}");
 assertThat(parsedJson.read("$.duck", Integer.class)).isEqualTo(123);
 assertThat(parsedJson.read("$.alpha", String.class)).matches("[\\p{L}]*");
 assertThat(parsedJson.read("$.alpha", String.class)).isEqualTo("abc");
 assertThat(parsedJson.read("$.number", String.class)).matches("-?(\\d*\\.\\d+|\\d+)");
 assertThat(parsedJson.read("$.aBoolean", String.class)).matches("(true|false)");
 assertThat(parsedJson.read("$.date", String.class)).matches("(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])");
 assertThat(parsedJson.read("$.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
 assertThat(parsedJson.read("$.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
 assertThat((Object) parsedJson.read("$.valueWithTypeMatch")).isInstanceOf(java.lang.String.class);
 assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class);
 assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMin", java.util.Collection.class)).as("$.valueWithMin").hasSizeGreaterThanOrEqualTo(1);
 assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class);
 assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMax", java.util.Collection.class)).as("$.valueWithMax").hasSizeLessThanOrEqualTo(3);
 assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class);
 assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinMax", java.util.Collection.class)).as("$.valueWithMinMax").hasSizeBetween(1, 3);
 assertThat((Object) parsedJson.read("$.valueWithMinEmpty")).isInstanceOf(java.util.List.class);
 assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinEmpty", java.util.Collection.class)).as("$.valueWithMinEmpty").hasSizeGreaterThanOrEqualTo(0);
 assertThat((Object) parsedJson.read("$.valueWithMaxEmpty")).isInstanceOf(java.util.List.class);
 assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMaxEmpty", java.util.Collection.class)).as("$.valueWithMaxEmpty").hasSizeLessThanOrEqualTo(0);
 assertThatValueIsANumber(parsedJson.read("$.duck"));
 assertThat(parsedJson.read("$.['key'].['complex.key']", String.class)).isEqualTo("foo");
请注意,对于该方法,该示例调用 .此方法必须在测试基类中定义,或者是 静态导入到您的测试中。请注意,该调用已转换为 .这意味着发动机需要 方法名称,并将正确的 JSON 路径作为参数传递给它。byCommandassertThatValueIsANumberbyCommandassertThatValueIsANumber(parsedJson.read("$.duck"));

生成的 WireMock 存根位于以下示例中:spring-doc.cn

                    '''
{
  "request" : {
    "urlPath" : "/get",
    "method" : "POST",
    "headers" : {
      "Content-Type" : {
        "matches" : "application/json.*"
      }
    },
    "bodyPatterns" : [ {
      "matchesJsonPath" : "$.['list'].['some'].['nested'][?(@.['anothervalue'] == 4)]"
    }, {
      "matchesJsonPath" : "$[?(@.['valueWithoutAMatcher'] == 'foo')]"
    }, {
      "matchesJsonPath" : "$[?(@.['valueWithTypeMatch'] == 'string')]"
    }, {
      "matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['json'] == 'with value')]"
    }, {
      "matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['anothervalue'] == 4)]"
    }, {
      "matchesJsonPath" : "$[?(@.duck =~ /([0-9]{3})/)]"
    }, {
      "matchesJsonPath" : "$[?(@.duck == 123)]"
    }, {
      "matchesJsonPath" : "$[?(@.alpha =~ /([\\\\p{L}]*)/)]"
    }, {
      "matchesJsonPath" : "$[?(@.alpha == 'abc')]"
    }, {
      "matchesJsonPath" : "$[?(@.number =~ /(-?(\\\\d*\\\\.\\\\d+|\\\\d+))/)]"
    }, {
      "matchesJsonPath" : "$[?(@.aBoolean =~ /((true|false))/)]"
    }, {
      "matchesJsonPath" : "$[?(@.date =~ /((\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))/)]"
    }, {
      "matchesJsonPath" : "$[?(@.dateTime =~ /(([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]"
    }, {
      "matchesJsonPath" : "$[?(@.time =~ /((2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]"
    }, {
      "matchesJsonPath" : "$.list.some.nested[?(@.json =~ /(.*)/)]"
    }, {
      "matchesJsonPath" : "$[?(@.valueWithMin.size() >= 1)]"
    }, {
      "matchesJsonPath" : "$[?(@.valueWithMax.size() <= 3)]"
    }, {
      "matchesJsonPath" : "$[?(@.valueWithMinMax.size() >= 1 && @.valueWithMinMax.size() <= 3)]"
    }, {
      "matchesJsonPath" : "$[?(@.valueWithOccurrence.size() >= 4 && @.valueWithOccurrence.size() <= 4)]"
    } ]
  },
  "response" : {
    "status" : 200,
    "body" : "{\\"duck\\":123,\\"alpha\\":\\"abc\\",\\"number\\":123,\\"aBoolean\\":true,\\"date\\":\\"2017-01-01\\",\\"dateTime\\":\\"2017-01-01T01:23:45\\",\\"time\\":\\"01:02:34\\",\\"valueWithoutAMatcher\\":\\"foo\\",\\"valueWithTypeMatch\\":\\"string\\",\\"valueWithMin\\":[1,2,3],\\"valueWithMax\\":[1,2,3],\\"valueWithMinMax\\":[1,2,3],\\"valueWithOccurrence\\":[1,2,3,4]}",
    "headers" : {
      "Content-Type" : "application/json"
    },
    "transformers" : [ "response-template", "spring-cloud-contract" ]
  }
}
'''
如果您使用 ,则请求和响应中具有 JSON 路径的地址将从断言中删除的部分。在 验证集合时,您必须为 收集。matchermatcher

请考虑以下示例:spring-doc.cn

Contract.make {
    request {
        method 'GET'
        url("/foo")
    }
    response {
        status OK()
        body(events: [[
                                 operation          : 'EXPORT',
                                 eventId            : '16f1ed75-0bcc-4f0d-a04d-3121798faf99',
                                 status             : 'OK'
                         ], [
                                 operation          : 'INPUT_PROCESSING',
                                 eventId            : '3bb4ac82-6652-462f-b6d1-75e424a0024a',
                                 status             : 'OK'
                         ]
                ]
        )
        bodyMatchers {
            jsonPath('$.events[0].operation', byRegex('.+'))
            jsonPath('$.events[0].eventId', byRegex('^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})$'))
            jsonPath('$.events[0].status', byRegex('.+'))
        }
    }
}

前面的代码导致创建以下测试(代码块仅显示 assertion 部分):spring-doc.cn

and:
    DocumentContext parsedJson = JsonPath.parse(response.body.asString())
    assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("16f1ed75-0bcc-4f0d-a04d-3121798faf99")
    assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("EXPORT")
    assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("INPUT_PROCESSING")
    assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("3bb4ac82-6652-462f-b6d1-75e424a0024a")
    assertThatJson(parsedJson).array("['events']").contains("['status']").isEqualTo("OK")
and:
    assertThat(parsedJson.read("\$.events[0].operation", String.class)).matches(".+")
    assertThat(parsedJson.read("\$.events[0].eventId", String.class)).matches("^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\$")
    assertThat(parsedJson.read("\$.events[0].status", String.class)).matches(".+")

请注意,该断言格式不正确。只有数组的第一个元素得到 断言。要解决此问题,请将断言应用于整个集合,并使用该方法对其进行断言。$.eventsbyCommand(…​)spring-doc.cn

2.5. 异步支持

如果您在服务器端使用异步通信(您的控制器是 return 、 、 等),那么,在您的 Contract 中,您必须 在 节 中提供方法。下面的代码显示了一个示例:CallableDeferredResultasync()responsespring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    request {
        method GET()
        url '/get'
    }
    response {
        status OK()
        body 'Passed'
        async()
    }
}
YAML
response:
    async: true
Java
class contract implements Supplier<Collection<Contract>> {

    @Override
    public Collection<Contract> get() {
        return Collections.singletonList(Contract.make(c -> {
            c.request(r -> {
                // ...
            });
            c.response(r -> {
                r.async();
                // ...
            });
        }));
    }

}
Kotlin
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

contract {
    request {
        // ...
    }
    response {
        async = true
        // ...
    }
}

您还可以使用 method 或 property 向存根添加延迟。 以下示例显示了如何执行此操作:fixedDelayMillisecondsspring-doc.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    request {
        method GET()
        url '/get'
    }
    response {
        status 200
        body 'Passed'
        fixedDelayMilliseconds 1000
    }
}
YAML
response:
    fixedDelayMilliseconds: 1000
Java
class contract implements Supplier<Collection<Contract>> {

    @Override
    public Collection<Contract> get() {
        return Collections.singletonList(Contract.make(c -> {
            c.request(r -> {
                // ...
            });
            c.response(r -> {
                r.fixedDelayMilliseconds(1000);
                // ...
            });
        }));
    }

}
Kotlin
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

contract {
    request {
        // ...
    }
    response {
        delay = fixedMilliseconds(1000)
        // ...
    }
}

2.6. XML HTTP 支持

对于 HTTP 协定,我们还支持在请求和响应正文中使用 XML。 XML 主体必须在元素中传递 作为 a 或 .此外,还可以提供身体匹配器 请求和响应。应使用方法代替方法,并将 desired 作为第一个参数提供 和 appropriate 作为第二个参数。支持除 之外的所有 body matcher。bodyStringGStringjsonPath(…​)org.springframework.cloud.contract.spec.internal.BodyMatchers.xPathxPathMatchingTypebyType()spring-doc.cn

下面的示例展示了一个 Groovy DSL 契约,响应正文中带有 XML:spring-doc.cn

槽的
                    Contract.make {
                        request {
                            method GET()
                            urlPath '/get'
                            headers {
                                contentType(applicationXml())
                            }
                        }
                        response {
                            status(OK())
                            headers {
                                contentType(applicationXml())
                            }
                            body """
<test>
<duck type='xtype'>123</duck>
<alpha>abc</alpha>
<list>
<elem>abc</elem>
<elem>def</elem>
<elem>ghi</elem>
</list>
<number>123</number>
<aBoolean>true</aBoolean>
<date>2017-01-01</date>
<dateTime>2017-01-01T01:23:45</dateTime>
<time>01:02:34</time>
<valueWithoutAMatcher>foo</valueWithoutAMatcher>
<key><complex>foo</complex></key>
</test>"""
                            bodyMatchers {
                                xPath('/test/duck/text()', byRegex("[0-9]{3}"))
                                xPath('/test/duck/text()', byCommand('equals($it)'))
                                xPath('/test/duck/xxx', byNull())
                                xPath('/test/duck/text()', byEquality())
                                xPath('/test/alpha/text()', byRegex(onlyAlphaUnicode()))
                                xPath('/test/alpha/text()', byEquality())
                                xPath('/test/number/text()', byRegex(number()))
                                xPath('/test/date/text()', byDate())
                                xPath('/test/dateTime/text()', byTimestamp())
                                xPath('/test/time/text()', byTime())
                                xPath('/test/*/complex/text()', byEquality())
                                xPath('/test/duck/@type', byEquality())
                            }
                        }
                    }
                    Contract.make {
                        request {
                            method GET()
                            urlPath '/get'
                            headers {
                                contentType(applicationXml())
                            }
                        }
                        response {
                            status(OK())
                            headers {
                                contentType(applicationXml())
                            }
                            body """
<ns1:test xmlns:ns1="http://demo.com/testns">
 <ns1:header>
    <duck-bucket type='bigbucket'>
      <duck>duck5150</duck>
    </duck-bucket>
</ns1:header>
</ns1:test>
"""
                            bodyMatchers {
                                xPath('/test/duck/text()', byRegex("[0-9]{3}"))
                                xPath('/test/duck/text()', byCommand('equals($it)'))
                                xPath('/test/duck/xxx', byNull())
                                xPath('/test/duck/text()', byEquality())
                                xPath('/test/alpha/text()', byRegex(onlyAlphaUnicode()))
                                xPath('/test/alpha/text()', byEquality())
                                xPath('/test/number/text()', byRegex(number()))
                                xPath('/test/date/text()', byDate())
                                xPath('/test/dateTime/text()', byTimestamp())
                                xPath('/test/time/text()', byTime())
                                xPath('/test/duck/@type', byEquality())
                            }
                        }
                    }
                    Contract.make {
                        request {
                            method GET()
                            urlPath '/get'
                            headers {
                                contentType(applicationXml())
                            }
                        }
                        response {
                            status(OK())
                            headers {
                                contentType(applicationXml())
                            }
                            body """
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header>
      <RsHeader xmlns="http://schemas.xmlsoap.org/soap/custom">
         <MsgSeqId>1234</MsgSeqId>
      </RsHeader>
   </SOAP-ENV:Header>
</SOAP-ENV:Envelope>
"""
                            bodyMatchers {
                                xPath('//*[local-name()=\'RsHeader\' and namespace-uri()=\'http://schemas.xmlsoap.org/soap/custom\']/*[local-name()=\'MsgSeqId\']/text()', byEquality())
                            }
                        }
                    }
                    Contract.make {
                        request {
                            method GET()
                            urlPath '/get'
                            headers {
                                contentType(applicationXml())
                            }
                        }
                        response {
                            status(OK())
                            headers {
                                contentType(applicationXml())
                            }
                            body """
<ns1:customer xmlns:ns1="http://demo.com/customer" xmlns:addr="http://demo.com/address">
    <email>[email protected]</email>
    <contact-info xmlns="http://demo.com/contact-info">
        <name>Krombopulous</name>
        <address>
            <addr:gps>
                <lat>51</lat>
                <addr:lon>50</addr:lon>
            </addr:gps>
        </address>
    </contact-info>
</ns1:customer>
"""
                        }
                    }
YAML
request:
  method: GET
  url: /getymlResponse
  headers:
    Content-Type: application/xml
  body: |
    <test>
    <duck type='xtype'>123</duck>
    <alpha>abc</alpha>
    <list>
    <elem>abc</elem>
    <elem>def</elem>
    <elem>ghi</elem>
    </list>
    <number>123</number>
    <aBoolean>true</aBoolean>
    <date>2017-01-01</date>
    <dateTime>2017-01-01T01:23:45</dateTime>
    <time>01:02:34</time>
    <valueWithoutAMatcher>foo</valueWithoutAMatcher>
    <valueWithTypeMatch>string</valueWithTypeMatch>
    <key><complex>foo</complex></key>
    </test>
  matchers:
    body:
      - path: /test/duck/text()
        type: by_regex
        value: "[0-9]{10}"
      - path: /test/duck/text()
        type: by_equality
      - path: /test/time/text()
        type: by_time
response:
  status: 200
  headers:
    Content-Type: application/xml
  body: |
    <test>
    <duck type='xtype'>123</duck>
    <alpha>abc</alpha>
    <list>
    <elem>abc</elem>
    <elem>def</elem>
    <elem>ghi</elem>
    </list>
    <number>123</number>
    <aBoolean>true</aBoolean>
    <date>2017-01-01</date>
    <dateTime>2017-01-01T01:23:45</dateTime>
    <time>01:02:34</time>
    <valueWithoutAMatcher>foo</valueWithoutAMatcher>
    <valueWithTypeMatch>string</valueWithTypeMatch>
    <key><complex>foo</complex></key>
    </test>
  matchers:
    body:
      - path: /test/duck/text()
        type: by_regex
        value: "[0-9]{10}"
      - path: /test/duck/text()
        type: by_command
        value: "test($it)"
      - path: /test/duck/xxx
        type: by_null
      - path: /test/duck/text()
        type: by_equality
      - path: /test/time/text()
        type: by_time
Java
import java.util.function.Supplier;

import org.springframework.cloud.contract.spec.Contract;

class contract_xml implements Supplier<Contract> {

    @Override
    public Contract get() {
        return Contract.make(c -> {
            c.request(r -> {
                r.method(r.GET());
                r.urlPath("/get");
                r.headers(h -> {
                    h.contentType(h.applicationXml());
                });
            });
            c.response(r -> {
                r.status(r.OK());
                r.headers(h -> {
                    h.contentType(h.applicationXml());
                });
                r.body("<test>\n" + "<duck type='xtype'>123</duck>\n" + "<alpha>abc</alpha>\n" + "<list>\n"
                        + "<elem>abc</elem>\n" + "<elem>def</elem>\n" + "<elem>ghi</elem>\n" + "</list>\n"
                        + "<number>123</number>\n" + "<aBoolean>true</aBoolean>\n" + "<date>2017-01-01</date>\n"
                        + "<dateTime>2017-01-01T01:23:45</dateTime>\n" + "<time>01:02:34</time>\n"
                        + "<valueWithoutAMatcher>foo</valueWithoutAMatcher>\n" + "<key><complex>foo</complex></key>\n"
                        + "</test>");
                r.bodyMatchers(m -> {
                    m.xPath("/test/duck/text()", m.byRegex("[0-9]{3}"));
                    m.xPath("/test/duck/text()", m.byCommand("equals($it)"));
                    m.xPath("/test/duck/xxx", m.byNull());
                    m.xPath("/test/duck/text()", m.byEquality());
                    m.xPath("/test/alpha/text()", m.byRegex(r.onlyAlphaUnicode()));
                    m.xPath("/test/alpha/text()", m.byEquality());
                    m.xPath("/test/number/text()", m.byRegex(r.number()));
                    m.xPath("/test/date/text()", m.byDate());
                    m.xPath("/test/dateTime/text()", m.byTimestamp());
                    m.xPath("/test/time/text()", m.byTime());
                    m.xPath("/test/*/complex/text()", m.byEquality());
                    m.xPath("/test/duck/@type", m.byEquality());
                });
            });
        });
    };

}
Kotlin
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

contract {
    request {
        method = GET
        urlPath = path("/get")
        headers {
            contentType = APPLICATION_XML
        }
    }
    response {
        status = OK
        headers {
            contentType =APPLICATION_XML
        }
        body = body("<test>\n" + "<duck type='xtype'>123</duck>\n"
                + "<alpha>abc</alpha>\n" + "<list>\n" + "<elem>abc</elem>\n"
                + "<elem>def</elem>\n" + "<elem>ghi</elem>\n" + "</list>\n"
                + "<number>123</number>\n" + "<aBoolean>true</aBoolean>\n"
                + "<date>2017-01-01</date>\n"
                + "<dateTime>2017-01-01T01:23:45</dateTime>\n"
                + "<time>01:02:34</time>\n"
                + "<valueWithoutAMatcher>foo</valueWithoutAMatcher>\n"
                + "<key><complex>foo</complex></key>\n" + "</test>")
        bodyMatchers {
            xPath("/test/duck/text()", byRegex("[0-9]{3}"))
            xPath("/test/duck/text()", byCommand("equals(\$it)"))
            xPath("/test/duck/xxx", byNull)
            xPath("/test/duck/text()", byEquality)
            xPath("/test/alpha/text()", byRegex(onlyAlphaUnicode))
            xPath("/test/alpha/text()", byEquality)
            xPath("/test/number/text()", byRegex(number))
            xPath("/test/date/text()", byDate)
            xPath("/test/dateTime/text()", byTimestamp)
            xPath("/test/time/text()", byTime)
            xPath("/test/*/complex/text()", byEquality)
            xPath("/test/duck/@type", byEquality)
        }
    }
}

以下示例显示了在响应正文中自动生成的 XML 测试:spring-doc.cn

@Test
public void validate_xmlMatches() throws Exception {
    // given:
    MockMvcRequestSpecification request = given()
                .header("Content-Type", "application/xml");

    // when:
    ResponseOptions response = given().spec(request).get("/get");

    // then:
    assertThat(response.statusCode()).isEqualTo(200);
    // and:
    DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance()
                    .newDocumentBuilder();
    Document parsedXml = documentBuilder.parse(new InputSource(
                new StringReader(response.getBody().asString())));
    // and:
    assertThat(valueFromXPath(parsedXml, "/test/list/elem/text()")).isEqualTo("abc");
    assertThat(valueFromXPath(parsedXml,"/test/list/elem[2]/text()")).isEqualTo("def");
    assertThat(valueFromXPath(parsedXml, "/test/duck/text()")).matches("[0-9]{3}");
    assertThat(nodeFromXPath(parsedXml, "/test/duck/xxx")).isNull();
    assertThat(valueFromXPath(parsedXml, "/test/alpha/text()")).matches("[\\p{L}]*");
    assertThat(valueFromXPath(parsedXml, "/test/*/complex/text()")).isEqualTo("foo");
    assertThat(valueFromXPath(parsedXml, "/test/duck/@type")).isEqualTo("xtype");
    }

2.6.1. XML 命名空间支持

支持命名空间 XML。但是,必须更新用于选择命名空间内容的任何 XPath 表达式。spring-doc.cn

请考虑以下显式命名空间的 XML 文档:spring-doc.cn

<ns1:customer xmlns:ns1="http://demo.com/customer">
    <email>[email protected]</email>
</ns1:customer>

用于选择电子邮件地址的 XPath 表达式为:./ns1:customer/email/text()spring-doc.cn

请注意,因为非限定表达式 () 会导致 。/customer/email/text()""

对于使用非限定命名空间的内容,表达式更详细。请考虑以下 XML 文档,该文档 使用非限定命名空间:spring-doc.cn

<customer xmlns="http://demo.com/customer">
    <email>[email protected]</email>
</customer>

用于选择电子邮件地址的 XPath 表达式为spring-doc.cn

*/[local-name()='customer' and namespace-uri()='http://demo.com/customer']/*[local-name()='email']/text()
请注意,因为非限定表达式 ( 或 ) 导致。甚至子元素也必须使用语法进行引用。/customer/email/text()*/[local-name()='customer' and namespace-uri()='http://demo.com/customer']/email/text()""local-name
通用命名空间节点表达式语法
/<node-name>
/*[local-name=()='<node-name>' and namespace-uri=()='<namespace-uri>']
在某些情况下,您可以省略该部分,但这样做可能会导致歧义。namespace_uri
  • Node 使用非限定命名空间(其祖先之一定义 xmlns 属性):spring-doc.cn

/*[local-name=()='<node-name>']

2.7. 一个文件中的多个合约

您可以在一个文件中定义多个合同。这样的合约可能类似于 以下示例:spring-doc.cn

槽的
import org.springframework.cloud.contract.spec.Contract

[
    Contract.make {
        name("should post a user")
        request {
            method 'POST'
            url('/users/1')
        }
        response {
            status OK()
        }
    },
    Contract.make {
        request {
            method 'POST'
            url('/users/2')
        }
        response {
            status OK()
        }
    }
]
YAML
---
name: should post a user
request:
  method: POST
  url: /users/1
response:
  status: 200
---
request:
  method: POST
  url: /users/2
response:
  status: 200
---
request:
  method: POST
  url: /users/3
response:
  status: 200
Java
class contract implements Supplier<Collection<Contract>> {

    @Override
    public Collection<Contract> get() {
        return Arrays.asList(
            Contract.make(c -> {
                c.name("should post a user");
                // ...
            }), Contract.make(c -> {
                // ...
            }), Contract.make(c -> {
                // ...
            })
        );
    }

}
Kotlin
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract

arrayOf(
    contract {
        name("should post a user")
        // ...
    },
    contract {
        // ...
    },
    contract {
        // ...
    }
}

在前面的示例中,一个 Contract 具有字段,而另一个 Contract 没有。这 导致生成两个测试,如下所示:namespring-doc.cn

package org.springframework.cloud.contract.verifier.tests.com.hello;

import com.example.TestBase;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
import com.jayway.restassured.response.ResponseOptions;
import org.junit.Test;

import static com.jayway.restassured.module.mockmvc.RestAssuredMockMvc.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.assertj.core.api.Assertions.assertThat;

public class V1Test extends TestBase {

    @Test
    public void validate_should_post_a_user() throws Exception {
        // given:
            MockMvcRequestSpecification request = given();

        // when:
            ResponseOptions response = given().spec(request)
                    .post("/users/1");

        // then:
            assertThat(response.statusCode()).isEqualTo(200);
    }

    @Test
    public void validate_withList_1() throws Exception {
        // given:
            MockMvcRequestSpecification request = given();

        // when:
            ResponseOptions response = given().spec(request)
                    .post("/users/2");

        // then:
            assertThat(response.statusCode()).isEqualTo(200);
    }

}

请注意,对于具有字段的 Contract,生成的测试方法名为 。没有该字段的名为 。它对应于文件名和 列表中合约的索引。namevalidate_should_post_a_usernamevalidate_withList_1WithList.groovyspring-doc.cn

生成的存根如以下示例所示:spring-doc.cn

should post a user.json
1_WithList.json

第一个文件从合约中获取参数。第二个 获取了以索引为前缀的合约文件 () 的名称(在此 的情况下,合约在文件的合约列表中有一个索引 )。nameWithList.groovy1spring-doc.cn

为您的合约命名要好得多,因为这样做会使 您的测试更有意义。

2.8. 有状态合约

有状态合同(也称为场景)是应该读取的合同定义 挨次。这在以下情况下可能很有用:spring-doc.cn

  • 您希望以精确定义的顺序调用合约,因为您使用 Spring Cloud Contract 来测试您的有状态应用程序。spring-doc.cn

我们真的不鼓励你这样做,因为 Contract 测试应该是无状态的。
  • 您希望同一终端节点为同一请求返回不同的结果。spring-doc.cn

要创建有状态合约(或场景),您需要 在创建合同时使用正确的命名约定。约定 要求包含订单号,后跟下划线。这无论如何都有效 了解您是使用 YAML 还是 Groovy。下面的清单显示了一个示例:spring-doc.cn

my_contracts_dir\
  scenario1\
    1_login.groovy
    2_showCart.groovy
    3_logout.groovy

这样的树会导致 Spring Cloud Contract Verifier 生成 WireMock 的场景,其中包含 name of 和以下三个步骤:scenario1spring-doc.cn

  1. login,标记为指向...Startedspring-doc.cn

  2. showCart,标记为指向...Step1spring-doc.cn

  3. logout,标记为 (这将关闭方案)。Step2spring-doc.cn

您可以在 https://wiremock.org/docs/stateful-behaviour/ 中找到有关 WireMock 方案的更多详细信息。spring-doc.cn

3. 集成

3.1. JAX-RS 系列

Spring Cloud Contract 支持 JAX-RS 2 客户端 API。基类需要 定义和服务器初始化。唯一的选项 测试 JAX-RS API 是为了启动 Web 服务器。此外,带有正文的请求需要有一个 content 类型。否则,将使用默认值。protected WebTarget webTargetapplication/octet-streamspring-doc.cn

要使用 JAX-RS 模式,请使用以下设置:spring-doc.cn

testMode = 'JAXRSCLIENT'

以下示例显示了生成的测试 API:spring-doc.cn

package com.example;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Response;

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static javax.ws.rs.client.Entity.*;

public class FooTest {
  WebTarget webTarget;

  @Test
  public void validate_() throws Exception {

    // when:
      Response response = webTarget
              .path("/users")
              .queryParam("limit", "10")
              .queryParam("offset", "20")
              .queryParam("filter", "email")
              .queryParam("sort", "name")
              .queryParam("search", "55")
              .queryParam("age", "99")
              .queryParam("name", "Denis.Stepanov")
              .queryParam("email", "[email protected]")
              .request()
              .build("GET")
              .invoke();
      String responseAsString = response.readEntity(String.class);

    // then:
      assertThat(response.getStatus()).isEqualTo(200);

    // and:
      DocumentContext parsedJson = JsonPath.parse(responseAsString);
      assertThatJson(parsedJson).field("['property1']").isEqualTo("a");
  }

}

3.2. 使用 WebTestClient 的 WebFlux

您可以使用 WebTestClient 来使用 WebFlux。以下清单显示了如何 配置 WebTestClient 作为测试模式:spring-doc.cn

Maven 系列
<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <testMode>WEBTESTCLIENT</testMode>
    </configuration>
</plugin>
Gradle
contracts {
        testMode = 'WEBTESTCLIENT'
}

以下示例演示如何设置 WebTestClient 基类和 RestAssure 对于 WebFlux:spring-doc.cn

import io.restassured.module.webtestclient.RestAssuredWebTestClient;
import org.junit.Before;

public abstract class BeerRestBase {

    @Before
    public void setup() {
        RestAssuredWebTestClient.standaloneSetup(
        new ProducerController(personToCheck -> personToCheck.age >= 20));
    }
}
}
该模式比该模式更快。WebTestClientEXPLICIT

3.3. 具有显式模式的 WebFlux

您还可以在生成的测试中将 WebFlux 与 explicit 模式一起使用 以使用 WebFlux。以下示例演示如何使用显式模式进行配置:spring-doc.cn

Maven 系列
<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <testMode>EXPLICIT</testMode>
    </configuration>
</plugin>
Gradle
contracts {
        testMode = 'EXPLICIT'
}

以下示例显示了如何为 Web Flux 设置基类和 RestAssured:spring-doc.cn

@SpringBootTest(classes = BeerRestBase.Config.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        properties = "server.port=0")
public abstract class BeerRestBase {

    // your tests go here

    // in this config class you define all controllers and mocked services
@Configuration
@EnableAutoConfiguration
static class Config {

    @Bean
    PersonCheckingService personCheckingService()  {
        return personToCheck -> personToCheck.age >= 20;
    }

    @Bean
    ProducerController producerController() {
        return new ProducerController(personCheckingService());
    }
}

}

3.4. 自定义模式

此模式是实验性的,将来可能会更改。

Spring Cloud Contract 允许您提供自己的自定义 .这样,您可以使用任何您想要发送和接收请求的客户端。Spring Cloud Contract 中的默认实现是,它使用 OkHttp3 http client。org.springframework.cloud.contract.verifier.http.HttpVerifierOkHttpHttpVerifierspring-doc.cn

要开始使用,请设置为 :testModeCUSTOMspring-doc.cn

testMode = 'CUSTOM'

以下示例显示了生成的测试:spring-doc.cn

package com.example.beer;

import com.example.BeerRestBase;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.http.HttpVerifier;
import org.springframework.cloud.contract.verifier.http.Request;
import org.springframework.cloud.contract.verifier.http.Response;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static org.springframework.cloud.contract.verifier.http.Request.given;

public class RestTest extends BeerRestBase {
    @Inject HttpVerifier httpVerifier;

    @Test
    public void validate_shouldGrantABeerIfOldEnough() throws Exception {
        // given:
            Request request = given()
                    .post("/beer.BeerService/check")
                    .scheme("HTTP")
                    .protocol("h2_prior_knowledge")
                    .header("Content-Type", "application/grpc")
                    .header("te", "trailers")
                    .body(fileToBytes(this, "shouldGrantABeerIfOldEnough_request_PersonToCheck_old_enough.bin"))
                    .build();


        // when:
            Response response = httpVerifier.exchange(request);


        // then:
            assertThat(response.statusCode()).isEqualTo(200);
            assertThat(response.header("Content-Type")).matches("application/grpc.*");
            assertThat(response.header("grpc-encoding")).isEqualTo("identity");
            assertThat(response.header("grpc-accept-encoding")).isEqualTo("gzip");

        // and:
            assertThat(response.getBody().asByteArray()).isEqualTo(fileToBytes(this, "shouldGrantABeerIfOldEnough_response_Response_old_enough.bin"));
    }

}

以下示例显示了相应的基类:spring-doc.cn

@SpringBootTest(classes = BeerRestBase.Config.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class BeerRestBase {

    @Configuration
    @EnableAutoConfiguration
    static class Config {

        @Bean
        ProducerController producerController(PersonCheckingService personCheckingService) {
            return new ProducerController(personCheckingService);
        }

        @Bean
        PersonCheckingService testPersonCheckingService() {
            return argument -> argument.getAge() >= 20;
        }

        @Bean
        HttpVerifier httpOkVerifier(@LocalServerPort int port) {
            return new OkHttpHttpVerifier("localhost:" + port);
        }

    }
}

3.5. 使用上下文路径

Spring Cloud Contract 支持上下文路径。spring-doc.cn

完全支持上下文路径所需的唯一更改是 producer 端。此外,自动生成的测试必须使用显式模式。消费者 侧面保持不变。为了使生成的测试通过,您必须使用 explicit 模式。以下示例显示如何将测试模式设置为 :EXPLICITspring-doc.cn

Maven 系列
<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <testMode>EXPLICIT</testMode>
    </configuration>
</plugin>
Gradle
contracts {
        testMode = 'EXPLICIT'
}

这样,您可以生成一个不使用 MockMvc 的测试。这意味着您生成 real 请求,并且您需要设置生成的测试的基类才能在真实的 插座。spring-doc.cn

考虑以下合约:spring-doc.cn

org.springframework.cloud.contract.spec.Contract.make {
    request {
        method 'GET'
        url '/my-context-path/url'
    }
    response {
        status OK()
    }
}

以下示例演示如何设置基类和 RestAssured:spring-doc.cn

import io.restassured.RestAssured;
import org.junit.Before;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(classes = ContextPathTestingBaseClass.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ContextPathTestingBaseClass {

    @LocalServerPort int port;

    @Before
    public void setup() {
        RestAssured.baseURI = "http://localhost";
        RestAssured.port = this.port;
    }
}

如果你这样做:spring-doc.cn

  • 自动生成的测试中的所有请求都会发送到真实终端节点,其中包含您的 包含上下文路径(例如,)。/my-context-path/urlspring-doc.cn

  • 您的合同反映您有一个上下文路径。您生成的存根还具有 该信息(例如,在存根中,您必须调用 )。/my-context-path/urlspring-doc.cn

3.6. 使用 REST 文档

您可以使用 Spring REST Docs 生成 使用 Spring MockMvc 的 HTTP API 的文档(例如,以 Asciidoc 格式), WebTestClient 或 RestAssure 的 Recover。在为 API 生成文档的同时,您还可以 使用 Spring Cloud Contract WireMock 生成 WireMock 存根。为此,请编写 普通的 REST Docs 测试用例,并且曾经有 stub 是 在 REST Docs 输出目录中自动生成。下面的 UML 图显示了 REST Docs 流程:@AutoConfigureRestDocsspring-doc.cn

REST 文档

以下示例使用 :MockMvcspring-doc.cn

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
public class ApplicationTests {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void contextLoads() throws Exception {
        mockMvc.perform(get("/resource"))
                .andExpect(content().string("Hello World"))
                .andDo(document("resource"));
    }
}

此测试在 处生成 WireMock 存根。它匹配 所有请求都发送到该路径。WebTestClient 的相同示例(使用 用于测试 Spring WebFlux 应用程序)将如下所示:target/snippets/stubs/resource.jsonGET/resourcespring-doc.cn

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureWebTestClient
public class ApplicationTests {

    @Autowired
    private WebTestClient client;

    @Test
    public void contextLoads() throws Exception {
        client.get().uri("/resource").exchange()
                .expectBody(String.class).isEqualTo("Hello World")
                .consumeWith(document("resource"));
    }
}

无需任何其他配置,这些测试将创建一个带有请求匹配器的存根 对于 HTTP 方法以及除 和 之外的所有标头。要匹配 request 更精确地(例如,为了匹配 POST 或 PUT 的正文),我们需要 显式创建请求匹配器。这样做有两个效果:hostcontent-lengthspring-doc.cn

此功能的主要入口点是 ,可以使用 作为便捷方法的替代方法,如下所示 示例显示:WireMockRestDocs.verify()document()spring-doc.cn

import static org.springframework.cloud.contract.wiremock.restdocs.WireMockRestDocs.verify;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
public class ApplicationTests {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void contextLoads() throws Exception {
        mockMvc.perform(post("/resource")
                .content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
                .andExpect(status().isOk())
                .andDo(verify().jsonPath("$.id"))
                .andDo(document("resource"));
    }
}

前面的 Contract 指定任何带有字段的有效 POST 都会收到响应 在此测试中定义。您可以将调用链接在一起以添加其他 匹配器。如果不熟悉 JSON 路径,则 JayWay 文档可以帮助您快速上手。此测试的 WebTestClient 版本 具有插入到同一位置的类似 static 辅助对象。id.jsonPath()verify()spring-doc.cn

除了 and 方便方法,您还可以使用 WireMock API 验证请求是否与创建的存根匹配,因为 以下示例显示:jsonPathcontentTypespring-doc.cn

@Test
public void contextLoads() throws Exception {
    mockMvc.perform(post("/resource")
            .content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
            .andExpect(status().isOk())
            .andDo(verify()
                    .wiremock(WireMock.post(urlPathEquals("/resource"))
                    .withRequestBody(matchingJsonPath("$.id"))
                    .andDo(document("post-resource"))));
}

WireMock API 非常丰富。您可以通过以下方式匹配标头、查询参数和请求正文 regex 以及 JSON 路径。您可以使用这些功能创建具有更宽 参数范围。前面的示例生成类似于以下示例的存根:spring-doc.cn

post-resource.json
{
  "request" : {
    "url" : "/resource",
    "method" : "POST",
    "bodyPatterns" : [ {
      "matchesJsonPath" : "$.id"
    }]
  },
  "response" : {
    "status" : 200,
    "body" : "Hello World",
    "headers" : {
      "X-Application-Context" : "application:-1",
      "Content-Type" : "text/plain"
    }
  }
}
您可以使用 method 或 and 方法创建请求匹配器,但不能同时使用这两种方法。wiremock()jsonPath()contentType()

在消费者端,您可以在本节前面进行生成的 available (例如,通过将存根发布为 JAR)。之后,您可以在 多种不同的方法,包括使用 ,如本文前面所述 公文。resource.json@AutoConfigureWireMock(stubs="classpath:resource.json")spring-doc.cn

3.6.1. 使用 REST 文档生成合约

您还可以使用 Spring REST 生成 Spring Cloud Contract DSL 文件和文档 文档。如果与 Spring Cloud WireMock 结合使用执行此操作,则会同时获得两个 Contract 和存根。spring-doc.cn

为什么要使用此功能?社区中的一些人提出了问题 关于他们希望转向基于 DSL 的合约定义的情况, 但是他们已经有很多 Spring MVC 测试了。使用此功能可让您生成 您稍后可以修改并移动到文件夹(在 configuration),以便插件找到它们。spring-doc.cn

您可能想知道为什么 WireMock 模块中有此功能。功能 存在,因为生成 Contract 和 Stub 都是有意义的。

请考虑以下测试:spring-doc.cn

        this.mockMvc
                .perform(post("/foo").accept(MediaType.APPLICATION_PDF).accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON).content("{\"foo\": 23, \"bar\" : \"baz\" }"))
                .andExpect(status().isOk()).andExpect(content().string("bar"))
                // first WireMock
                .andDo(WireMockRestDocs.verify().jsonPath("$[?(@.foo >= 20)]")
                        .jsonPath("$[?(@.bar in ['baz','bazz','bazzz'])]")
                        .contentType(MediaType.valueOf("application/json")))
                // then Contract DSL documentation
                .andDo(document("index", SpringCloudContractRestDocs.dslContract()));

前面的测试会创建上一节中介绍的存根,并生成 合同和文档文件。spring-doc.cn

调用 Contract 并可能类似于以下示例:index.groovyspring-doc.cn

import org.springframework.cloud.contract.spec.Contract

Contract.make {
    request {
        method 'POST'
        url '/foo'
        body('''
            {"foo": 23 }
        ''')
        headers {
            header('''Accept''', '''application/json''')
            header('''Content-Type''', '''application/json''')
        }
    }
    response {
        status OK()
        body('''
        bar
        ''')
        headers {
            header('''Content-Type''', '''application/json;charset=UTF-8''')
            header('''Content-Length''', '''3''')
        }
        bodyMatchers {
            jsonPath('$[?(@.foo >= 20)]', byType())
        }
    }
}

生成的文档(在本例中为 Asciidoc 格式)包含一个格式化的 合同。此文件的位置将为 。index/dsl-contract.adocspring-doc.cn

3.7. 图形QL

由于 GraphQL 本质上是 HTTP,因此您可以通过创建标准 HTTP 合同来为其编写合同,其中包含一个带有 key 和 mapping 的附加条目。metadataverifiertool=graphqlspring-doc.cn

槽的
import org.springframework.cloud.contract.spec.Contract

Contract.make {

    request {
        method(POST())
        url("/graphql")
        headers {
            contentType("application/json")
        }
        body('''
{
    "query":"query queryName($personName: String!) {\\n  personToCheck(name: $personName) {\\n    name\\n    age\\n  }\\n}\\n\\n\\n\\n",
    "variables":{"personName":"Old Enough"},
    "operationName":"queryName"
}
''')
    }

    response {
        status(200)
        headers {
            contentType("application/json")
        }
        body('''\
{
  "data": {
    "personToCheck": {
      "name": "Old Enough",
      "age": "40"
    }
  }
}
''')
    }
    metadata(verifier: [
            tool: "graphql"
    ])
}
YAML
---
request:
  method: "POST"
  url: "/graphql"
  headers:
    Content-Type: "application/json"
  body:
    query: "query queryName($personName: String!) { personToCheck(name: $personName)
      {         name    age  } }"
    variables:
      personName: "Old Enough"
    operationName: "queryName"
  matchers:
    headers:
      - key: "Content-Type"
        regex: "application/json.*"
        regexType: "as_string"
response:
  status: 200
  headers:
    Content-Type: "application/json"
  body:
    data:
      personToCheck:
        name: "Old Enough"
        age: "40"
  matchers:
    headers:
      - key: "Content-Type"
        regex: "application/json.*"
        regexType: "as_string"
name: "shouldRetrieveOldEnoughPerson"
metadata:
  verifier:
    tool: "graphql"

添加 metadata 部分将更改默认 WireMock 存根的构建方式。它现在将使用 Spring Cloud Contract 请求匹配器,例如,通过忽略空格将 GraphQL 请求的部分与真实请求进行比较。queryspring-doc.cn

3.7.1. 生产者端设置

在创建者方面,您的配置可能如下所示。spring-doc.cn

Maven 系列
<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <testMode>EXPLICIT</testMode>
        <baseClassForTests>com.example.BaseClass</baseClassForTests>
    </configuration>
</plugin>
Gradle
contracts {
    testMode = "EXPLICIT"
    baseClassForTests = "com.example.BaseClass"
}

基类将设置在随机端口上运行的应用程序。spring-doc.cn

基类
@SpringBootTest(classes = ProducerApplication.class,
        properties = "graphql.servlet.websocket.enabled=false",
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class BaseClass {

    @LocalServerPort int port;

    @BeforeEach
    public void setup() {
        RestAssured.baseURI = "http://localhost:" + port;
    }
}

3.7.2. 消费者端设置

GraphQL API 的使用者端测试示例。spring-doc.cn

消费者端测试
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class BeerControllerGraphQLTest {

    @RegisterExtension
    static StubRunnerExtension rule = new StubRunnerExtension()
            .downloadStub("com.example","beer-api-producer-graphql")
            .stubsMode(StubRunnerProperties.StubsMode.LOCAL);

    private static final String REQUEST_BODY = "{\n"
            + "\"query\":\"query queryName($personName: String!) {\\n  personToCheck(name: $personName) {\\n    name\\n    age\\n  }\\n}\","
            + "\"variables\":{\"personName\":\"Old Enough\"},\n"
            + "\"operationName\":\"queryName\"\n"
            + "}";

    @Test
    public void should_send_a_graphql_request() {
        ResponseEntity<String> responseEntity = new RestTemplate()
                .exchange(RequestEntity
                        .post(URI.create("http://localhost:" + rule.findStubUrl("beer-api-producer-graphql").getPort() + "/graphql"))
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(REQUEST_BODY), String.class);

        BDDAssertions.then(responseEntity.getStatusCodeValue()).isEqualTo(200);

    }
}

3.8. GRPC

GRPC 是一个构建在 HTTP/2 之上的 RPC 框架,Spring Cloud Contract 对此提供了基本支持。spring-doc.cn

Spring Cloud Contract 对 GRPC 的基本用例提供了实验性支持。不幸的是,由于 GRPC 对 HTTP/2 标头帧进行了调整,因此无法断言标头。grpc-status

让我们看看下面的合约。spring-doc.cn

Groovy 合约
package contracts.beer.rest


import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.http.ContractVerifierHttpMetaData

Contract.make {
    description("""
Represents a successful scenario of getting a beer

```
given:
    client is old enough
when:
    he applies for a beer
then:
    we'll grant him the beer
```

""")
    request {
        method 'POST'
        url '/beer.BeerService/check'
        body(fileAsBytes("PersonToCheck_old_enough.bin"))
        headers {
            contentType("application/grpc")
            header("te", "trailers")
        }
    }
    response {
        status 200
        body(fileAsBytes("Response_old_enough.bin"))
        headers {
            contentType("application/grpc")
            header("grpc-encoding", "identity")
            header("grpc-accept-encoding", "gzip")
        }
    }
    metadata([
            "verifierHttp": [
                    "protocol": ContractVerifierHttpMetaData.Protocol.H2_PRIOR_KNOWLEDGE.toString()
            ]
    ])
}

3.8.1. Producer 端设置

为了利用 HTTP/2 支持,您必须按如下方式设置测试模式。CUSTOMspring-doc.cn

Maven 系列
<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <testMode>CUSTOM</testMode>
        <packageWithBaseClasses>com.example</packageWithBaseClasses>
    </configuration>
</plugin>
Gradle
contracts {
    packageWithBaseClasses = 'com.example'
    testMode = "CUSTOM"
}

基类将设置在随机端口上运行的应用程序。它还会将 implementation 设置为可以使用 HTTP/2 协议的实现。Spring Cloud Contract 附带了 implementation。HttpVerifierOkHttpHttpVerifierspring-doc.cn

基类
@SpringBootTest(classes = BeerRestBase.Config.class,
        webEnvironment = SpringBootTest.WebEnvironment.NONE,
        properties = {
                "grpc.server.port=0"
        })
public abstract class BeerRestBase {

    @Autowired
    GrpcServerProperties properties;

    @Configuration
    @EnableAutoConfiguration
    static class Config {

        @Bean
        ProducerController producerController(PersonCheckingService personCheckingService) {
            return new ProducerController(personCheckingService);
        }

        @Bean
        PersonCheckingService testPersonCheckingService() {
            return argument -> argument.getAge() >= 20;
        }

        @Bean
        HttpVerifier httpOkVerifier(GrpcServerProperties properties) {
            return new OkHttpHttpVerifier("localhost:" + properties.getPort());
        }

    }
}

3.8.2. 消费者端设置

GRPC 消费者端测试示例。由于 GRPC 服务器端的异常行为,存根无法在适当的时候返回标头。这就是为什么我们需要手动设置退货状态的原因。grpc-statusspring-doc.cn

消费者端测试
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = GrpcTests.TestConfiguration.class, properties = {
        "grpc.client.beerService.address=static://localhost:5432", "grpc.client.beerService.negotiationType=TLS"
})
public class GrpcTests {

    @GrpcClient(value = "beerService", interceptorNames = "fixedStatusSendingClientInterceptor")
    BeerServiceGrpc.BeerServiceBlockingStub beerServiceBlockingStub;

    int port;

    @RegisterExtension
    static StubRunnerExtension rule = new StubRunnerExtension()
            .downloadStub("com.example", "beer-api-producer-grpc")
            // With WireMock PlainText mode you can just set an HTTP port
//          .withPort(5432)
            .stubsMode(StubRunnerProperties.StubsMode.LOCAL)
            .withHttpServerStubConfigurer(MyWireMockConfigurer.class);

    @BeforeEach
    public void setupPort() {
        this.port = rule.findStubUrl("beer-api-producer-grpc").getPort();
    }

    @Test
    public void should_give_me_a_beer_when_im_old_enough() throws Exception {
        Response response = beerServiceBlockingStub.check(PersonToCheck.newBuilder().setAge(23).build());

        BDDAssertions.then(response.getStatus()).isEqualTo(Response.BeerCheckStatus.OK);
    }

    @Test
    public void should_reject_a_beer_when_im_too_young() throws Exception {
        Response response = beerServiceBlockingStub.check(PersonToCheck.newBuilder().setAge(17).build());
        response = response == null ? Response.newBuilder().build() : response;

        BDDAssertions.then(response.getStatus()).isEqualTo(Response.BeerCheckStatus.NOT_OK);
    }

    // Not necessary with WireMock PlainText mode
    static class MyWireMockConfigurer extends WireMockHttpServerStubConfigurer {
        @Override
        public WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) {
            return httpStubConfiguration
                    .httpsPort(5432);
        }
    }

    @Configuration
    @ImportAutoConfiguration(GrpcClientAutoConfiguration.class)
    static class TestConfiguration {

        // Not necessary with WireMock PlainText mode
        @Bean
        public GrpcChannelConfigurer keepAliveClientConfigurer() {
            return (channelBuilder, name) -> {
                if (channelBuilder instanceof NettyChannelBuilder) {
                    try {
                        ((NettyChannelBuilder) channelBuilder)
                                .sslContext(GrpcSslContexts.forClient()
                                        .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                        .build());
                    }
                    catch (SSLException e) {
                        throw new IllegalStateException(e);
                    }
                }
            };
        }

        /**
         * GRPC client interceptor that sets the returned status always to OK.
         * You might want to change the return status depending on the received stub payload.
         *
         * Hopefully in the future this will be unnecessary and will be removed.
         */
        @Bean
        ClientInterceptor fixedStatusSendingClientInterceptor() {
            return new ClientInterceptor() {
                @Override
                public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
                    ClientCall<ReqT, RespT> call = next.newCall(method, callOptions);
                    return new ClientCall<ReqT, RespT>() {
                        @Override
                        public void start(Listener<RespT> responseListener, Metadata headers) {
                            Listener<RespT> listener = new Listener<RespT>() {
                                @Override
                                public void onHeaders(Metadata headers) {
                                    responseListener.onHeaders(headers);
                                }

                                @Override
                                public void onMessage(RespT message) {
                                    responseListener.onMessage(message);
                                }

                                @Override
                                public void onClose(Status status, Metadata trailers) {
                                    // TODO: This must be fixed somehow either in Jetty (WireMock) or somewhere else
                                    responseListener.onClose(Status.OK, trailers);
                                }

                                @Override
                                public void onReady() {
                                    responseListener.onReady();
                                }
                            };
                            call.start(listener, headers);
                        }

                        @Override
                        public void request(int numMessages) {
                            call.request(numMessages);
                        }

                        @Override
                        public void cancel(@Nullable String message, @Nullable Throwable cause) {
                            call.cancel(message, cause);
                        }

                        @Override
                        public void halfClose() {
                            call.halfClose();
                        }

                        @Override
                        public void sendMessage(ReqT message) {
                            call.sendMessage(message);
                        }
                    };
                }
            };
        }
    }
}

4. 消息

Spring Cloud Contract 允许您验证使用消息传递作为 通讯方式。本文档中展示的所有集成都可以与 Spring 一起使用。 但你也可以创建自己的一个并使用它。spring-doc.cn

4.1. 消息传递 DSL 顶级元素

用于消息传递的 DSL 看起来与专注于 HTTP 的 DSL 略有不同。这 以下部分解释了差异:spring-doc.cn

4.1.1. 由方法触发的输出

输出消息可以通过调用方法(例如,当合约是 started 和发送消息时),如以下示例所示:Schedulerspring-doc.cn

槽的
def dsl = Contract.make {
    // Human readable description
    description 'Some description'
    // Label by means of which the output message can be triggered
    label 'some_label'
    // input to the contract
    input {
        // the contract will be triggered by a method
        triggeredBy('bookReturnedTriggered()')
    }
    // output message of the contract
    outputMessage {
        // destination to which the output message will be sent
        sentTo('output')
        // the body of the output message
        body('''{ "bookName" : "foo" }''')
        // the headers of the output message
        headers {
            header('BOOK-NAME', 'foo')
        }
    }
}
YAML
# Human readable description
description: Some description
# Label by means of which the output message can be triggered
label: some_label
input:
  # the contract will be triggered by a method
  triggeredBy: bookReturnedTriggered()
# output message of the contract
outputMessage:
  # destination to which the output message will be sent
  sentTo: output
  # the body of the output message
  body:
    bookName: foo
  # the headers of the output message
  headers:
    BOOK-NAME: foo

在前面的示例案例中,如果调用了调用的方法,则会发送输出消息。在消息发布者方面,我们生成一个 test 调用该方法来触发消息。在使用者端,您可以使用 来触发消息。outputbookReturnedTriggeredsome_labelspring-doc.cn

4.1.2. 消息触发的输出

可以通过接收消息来触发输出消息,如下所示 例:spring-doc.cn

槽的
def dsl = Contract.make {
    description 'Some Description'
    label 'some_label'
    // input is a message
    input {
        // the message was received from this destination
        messageFrom('input')
        // has the following body
        messageBody([
                bookName: 'foo'
        ])
        // and the following headers
        messageHeaders {
            header('sample', 'header')
        }
    }
    outputMessage {
        sentTo('output')
        body([
                bookName: 'foo'
        ])
        headers {
            header('BOOK-NAME', 'foo')
        }
    }
}
YAML
# Human readable description
description: Some description
# Label by means of which the output message can be triggered
label: some_label
# input is a message
input:
  messageFrom: input
  # has the following body
  messageBody:
    bookName: 'foo'
  # and the following headers
  messageHeaders:
    sample: 'header'
# output message of the contract
outputMessage:
  # destination to which the output message will be sent
  sentTo: output
  # the body of the output message
  body:
    bookName: foo
  # the headers of the output message
  headers:
    BOOK-NAME: foo

在前面的示例中,如果正确的消息是 在目标上收到。在消息发布者方面,引擎 生成一个测试,将输入消息发送到定义的目标。在 Consumer 端,您可以向 Input 目标发送消息或使用标签 (在示例中) 触发消息。outputinputsome_labelspring-doc.cn

4.1.3. 消费者/生产者

此部分仅对 Groovy DSL 有效。

在 HTTP 中,你有一个 // 表示法的概念。您还可以 在消息传递中使用这些范例。此外,Spring Cloud Contract Verifier 还 提供了 and 方法,如以下示例所示 (注意,你可以使用 or 方法提供 and 部分):clientstub and `servertestconsumerproducer$valueconsumerproducerspring-doc.cn

                    Contract.make {
                name "foo"
                        label 'some_label'
                        input {
                            messageFrom value(consumer('jms:output'), producer('jms:input'))
                            messageBody([
                                    bookName: 'foo'
                            ])
                            messageHeaders {
                                header('sample', 'header')
                            }
                        }
                        outputMessage {
                            sentTo $(consumer('jms:input'), producer('jms:output'))
                            body([
                                    bookName: 'foo'
                            ])
                        }
                    }

4.1.4. 普通

在 or 部分中,您可以使用名称 的 (例如,) 中,您在 基类或静态导入。Spring Cloud Contract 运行该方法 在生成的测试中。inputoutputMessageassertThatmethodassertThatMessageIsOnTheQueue()spring-doc.cn

4.2. 集成

您可以使用以下四种集成配置之一:spring-doc.cn

由于我们使用 Spring Boot,如果您已将这些库之一添加到 Classpath 中,则所有 消息收发配置是自动设置的。spring-doc.cn

记得把你的 生成的测试。否则,Spring Cloud Contract 的消息传递部分不会 工作。@AutoConfigureMessageVerifier

如果要使用 Spring Cloud Stream,请记得在 上添加测试依赖,如下所示:org.springframework.cloud:spring-cloud-streamspring-doc.cn

Maven 系列
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream</artifactId>
    <type>test-jar</type>
    <scope>test</scope>
    <classifier>test-binder</classifier>
</dependency>
Gradle
testImplementation(group: 'org.springframework.cloud', name: 'spring-cloud-stream', classifier: 'test-binder')

4.2.1. 手动集成测试

测试使用的主要界面是 . 它定义了如何发送和接收消息。您可以创建自己的实现 实现相同的目标。org.springframework.cloud.contract.verifier.messaging.MessageVerifierspring-doc.cn

在测试中,您可以注入 a 来发送和接收 遵循 Contract 的消息。然后添加到您的测试中。 以下示例显示了如何执行此操作:ContractVerifierMessageExchange@AutoConfigureMessageVerifierspring-doc.cn

@RunWith(SpringTestRunner.class)
@SpringBootTest
@AutoConfigureMessageVerifier
public static class MessagingContractTests {

  @Autowired
  private MessageVerifier verifier;
  ...
}
如果您的测试也需要存根,则包括 消息传送配置,因此您只需要一个注释。@AutoConfigureStubRunner

4.3. 生产者端消息传递测试生成

在 DSL 中包含 or 部分会导致创建测试 在出版商方面。默认情况下,将创建 JUnit 4 测试。但是,还有一个 可以创建 JUnit 5、TestNG 或 Spock 测试。inputoutputMessagespring-doc.cn

我们应该考虑三种主要情况:spring-doc.cn

  • 方案 1:没有生成输出消息的输入消息。输出 message 由应用程序内的组件(例如,调度程序)触发。spring-doc.cn

  • 场景 2:输入消息触发输出消息。spring-doc.cn

  • 场景 3:输入消息被消费,没有输出消息。spring-doc.cn

传递给的目标或可以具有不同的 不同消息传递实现的含义。对于 Stream 和 Integration,它是 首先解析为 A of a 通道。那么,如果没有这样的 , 它被解析为通道名称。对于 Camel,这是一个特定的组件(例如,)。messageFromsentTodestinationdestinationjms

4.3.1. 场景 1:无输入消息

考虑以下合约:spring-doc.cn

槽的
def contractDsl = Contract.make {
    name "foo"
    label 'some_label'
    input {
        triggeredBy('bookReturnedTriggered()')
    }
    outputMessage {
        sentTo('activemq:output')
        body('''{ "bookName" : "foo" }''')
        headers {
            header('BOOK-NAME', 'foo')
            messagingContentType(applicationJson())
        }
    }
}
YAML
label: some_label
input:
  triggeredBy: bookReturnedTriggered
outputMessage:
  sentTo: activemq:output
  body:
    bookName: foo
  headers:
    BOOK-NAME: foo
    contentType: application/json

对于前面的示例,将创建以下测试:spring-doc.cn

JUnit
package com.example;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes;

public class FooTest {
    @Inject ContractVerifierMessaging contractVerifierMessaging;
    @Inject ContractVerifierObjectMapper contractVerifierObjectMapper;

    @Test
    public void validate_foo() throws Exception {
        // when:
            bookReturnedTriggered();

        // then:
            ContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output",
                    contract(this, "foo.yml"));
            assertThat(response).isNotNull();

        // and:
            assertThat(response.getHeader("BOOK-NAME")).isNotNull();
            assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
            assertThat(response.getHeader("contentType")).isNotNull();
            assertThat(response.getHeader("contentType").toString()).isEqualTo("application/json");

        // and:
            DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
            assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo");
    }

}
斯波克
package com.example

import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import spock.lang.Specification
import javax.inject.Inject
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes

class FooSpec extends Specification {
    @Inject ContractVerifierMessaging contractVerifierMessaging
    @Inject ContractVerifierObjectMapper contractVerifierObjectMapper

    def validate_foo() throws Exception {
        when:
            bookReturnedTriggered()

        then:
            ContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output",
                    contract(this, "foo.yml"))
            response != null

        and:
            response.getHeader("BOOK-NAME") != null
            response.getHeader("BOOK-NAME").toString() == 'foo'
            response.getHeader("contentType") != null
            response.getHeader("contentType").toString() == 'application/json'

        and:
            DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
            assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo")
    }

}

4.3.2. 场景 2:由输入触发的输出

考虑以下合约:spring-doc.cn

槽的
def contractDsl = Contract.make {
    name "foo"
    label 'some_label'
    input {
        messageFrom('jms:input')
        messageBody([
                bookName: 'foo'
        ])
        messageHeaders {
            header('sample', 'header')
        }
    }
    outputMessage {
        sentTo('jms:output')
        body([
                bookName: 'foo'
        ])
        headers {
            header('BOOK-NAME', 'foo')
        }
    }
}
YAML
label: some_label
input:
  messageFrom: jms:input
  messageBody:
    bookName: 'foo'
  messageHeaders:
    sample: header
outputMessage:
  sentTo: jms:output
  body:
    bookName: foo
  headers:
    BOOK-NAME: foo

对于前面的 Contract,将创建以下测试:spring-doc.cn

JUnit
package com.example;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes;

public class FooTest {
    @Inject ContractVerifierMessaging contractVerifierMessaging;
    @Inject ContractVerifierObjectMapper contractVerifierObjectMapper;

    @Test
    public void validate_foo() throws Exception {
        // given:
            ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
                    "{\\"bookName\\":\\"foo\\"}"
                        , headers()
                            .header("sample", "header")
            );

        // when:
            contractVerifierMessaging.send(inputMessage, "jms:input",
                    contract(this, "foo.yml"));

        // then:
            ContractVerifierMessage response = contractVerifierMessaging.receive("jms:output",
                    contract(this, "foo.yml"));
            assertThat(response).isNotNull();

        // and:
            assertThat(response.getHeader("BOOK-NAME")).isNotNull();
            assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");

        // and:
            DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
            assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo");
    }

}
斯波克
package com.example

import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import spock.lang.Specification
import javax.inject.Inject
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes

class FooSpec extends Specification {
    @Inject ContractVerifierMessaging contractVerifierMessaging
    @Inject ContractVerifierObjectMapper contractVerifierObjectMapper

    def validate_foo() throws Exception {
        given:
            ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
                    '''{"bookName":"foo"}'''
                        , headers()
                            .header("sample", "header")
            )

        when:
            contractVerifierMessaging.send(inputMessage, "jms:input",
                    contract(this, "foo.yml"))

        then:
            ContractVerifierMessage response = contractVerifierMessaging.receive("jms:output",
                    contract(this, "foo.yml"))
            response != null

        and:
            response.getHeader("BOOK-NAME") != null
            response.getHeader("BOOK-NAME").toString() == 'foo'

        and:
            DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
            assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo")
    }

}

4.3.3. 场景 3:无输出消息

考虑以下合约:spring-doc.cn

槽的
def contractDsl = Contract.make {
    name "foo"
    label 'some_label'
    input {
        messageFrom('jms:delete')
        messageBody([
                bookName: 'foo'
        ])
        messageHeaders {
            header('sample', 'header')
        }
        assertThat('bookWasDeleted()')
    }
}
YAML
label: some_label
input:
  messageFrom: jms:delete
  messageBody:
    bookName: 'foo'
  messageHeaders:
    sample: header
  assertThat: bookWasDeleted()

对于前面的 Contract,将创建以下测试:spring-doc.cn

JUnit
package com.example;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes;

public class FooTest {
    @Inject ContractVerifierMessaging contractVerifierMessaging;
    @Inject ContractVerifierObjectMapper contractVerifierObjectMapper;

    @Test
    public void validate_foo() throws Exception {
        // given:
            ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
                    "{\\"bookName\\":\\"foo\\"}"
                        , headers()
                            .header("sample", "header")
            );

        // when:
            contractVerifierMessaging.send(inputMessage, "jms:delete",
                    contract(this, "foo.yml"));
            bookWasDeleted();

    }

}
斯波克
package com.example

import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import spock.lang.Specification
import javax.inject.Inject
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes

class FooSpec extends Specification {
    @Inject ContractVerifierMessaging contractVerifierMessaging
    @Inject ContractVerifierObjectMapper contractVerifierObjectMapper

    def validate_foo() throws Exception {
        given:
            ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
                    '''{"bookName":"foo"}'''
                        , headers()
                            .header("sample", "header")
            )

        when:
            contractVerifierMessaging.send(inputMessage, "jms:delete",
                    contract(this, "foo.yml"))
            bookWasDeleted()

        then:
            noExceptionThrown()
    }

}

4.4. Consumer Stub 生成

与 HTTP 部分不同,在消息传递中,我们需要在 JAR 中发布合约定义,并且 一个存根。然后在使用者端对其进行解析,并创建适当的存根路由。spring-doc.cn

如果您在 Classpath 上有多个框架,则 Stub Runner 需要 定义应该使用哪一个。假设您拥有 AMQP、Spring Cloud Stream 和 Spring Integration 在 Classpath 上,并且您希望使用 Spring AMQP。然后,您需要设置 和 。 这样,唯一剩下的框架就是 Spring AMQP。stubrunner.stream.enabled=falsestubrunner.integration.enabled=false

4.4.1. 存根触发

要触发消息,请使用该接口,如下例所示:StubTriggerspring-doc.cn

package org.springframework.cloud.contract.stubrunner;

import java.util.Collection;
import java.util.Map;

/**
 * Contract for triggering stub messages.
 *
 * @author Marcin Grzejszczak
 */
public interface StubTrigger {

    /**
     * Triggers an event by a given label for a given {@code groupid:artifactid} notation.
     * You can use only {@code artifactId} too.
     *
     * Feature related to messaging.
     * @param ivyNotation ivy notation of a stub
     * @param labelName name of the label to trigger
     * @return true - if managed to run a trigger
     */
    boolean trigger(String ivyNotation, String labelName);

    /**
     * Triggers an event by a given label.
     *
     * Feature related to messaging.
     * @param labelName name of the label to trigger
     * @return true - if managed to run a trigger
     */
    boolean trigger(String labelName);

    /**
     * Triggers all possible events.
     *
     * Feature related to messaging.
     * @return true - if managed to run a trigger
     */
    boolean trigger();

    /**
     * Feature related to messaging.
     * @return a mapping of ivy notation of a dependency to all the labels it has.
     */
    Map<String, Collection<String>> labels();

}

为方便起见,接口扩展了 ,因此您只需要一个 或测试中的 other。StubFinderStubTriggerspring-doc.cn

StubTrigger提供了以下触发消息的选项:spring-doc.cn

4.4.2. 按标签触发

以下示例显示如何触发带有标签的消息:spring-doc.cn

stubFinder.trigger('return_book_1')

4.4.3. 按 Group 和 Artifact ID 触发

以下示例显示如何按组和构件 ID 触发消息:spring-doc.cn

stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:streamService', 'return_book_1')

4.4.4. 由 Artifact ID 触发

以下示例显示如何从工件 ID 触发消息:spring-doc.cn

stubFinder.trigger('streamService', 'return_book_1')

4.4.5. 触发所有消息

以下示例演示如何触发所有消息:spring-doc.cn

stubFinder.trigger()

4.5. 使用 Apache Camel 进行消费者端消息传递

Spring Cloud Contract Stub Runner 的消息传递模块为您提供了一种与 Apache Camel 集成的简便方法。 对于提供的工件,它会自动下载存根并注册所需的 路线。spring-doc.cn

4.5.1. 将 Apache Camel 添加到项目中

您可以在 Classpath 上同时拥有 Apache Camel 和 Spring Cloud Contract Stub Runner。 请记住使用 .@AutoConfigureStubRunnerspring-doc.cn

4.5.2. 禁用该功能

如果需要禁用此功能,请设置stubrunner.camel.enabled=falsespring-doc.cn

4.5.3. 示例

假设我们有以下 Maven 存储库,其中包含为应用程序部署的存根:camelServicespring-doc.cn

└── .m2
    └── repository
        └── io
            └── codearte
                └── accurest
                    └── stubs
                        └── camelService
                            ├── 0.0.1-SNAPSHOT
                            │   ├── camelService-0.0.1-SNAPSHOT.pom
                            │   ├── camelService-0.0.1-SNAPSHOT-stubs.jar
                            │   └── maven-metadata-local.xml
                            └── maven-metadata-local.xml

进一步假设存根包含以下结构:spring-doc.cn

├── META-INF
│   └── MANIFEST.MF
└── repository
    ├── accurest
    │   ├── bookDeleted.groovy
    │   ├── bookReturned1.groovy
    │   └── bookReturned2.groovy
    └── mappings

现在考虑以下合约(我们给它们编号 1 和 2):spring-doc.cn

Contract.make {
    label 'return_book_1'
    input {
        triggeredBy('bookReturnedTriggered()')
    }
    outputMessage {
        sentTo('jms:output')
        body('''{ "bookName" : "foo" }''')
        headers {
            header('BOOK-NAME', 'foo')
        }
    }
}
Contract.make {
    label 'return_book_2'
    input {
        messageFrom('jms:input')
        messageBody([
                bookName: 'foo'
        ])
        messageHeaders {
            header('sample', 'header')
        }
    }
    outputMessage {
        sentTo('jms:output')
        body([
                bookName: 'foo'
        ])
        headers {
            header('BOOK-NAME', 'foo')
        }
    }
}

这些示例适用于三种情况:spring-doc.cn

场景 1 (无输入消息)

要从标签触发消息,我们使用接口,如下所示:return_book_1StubTriggerspring-doc.cn

stubFinder.trigger('return_book_1')

接下来,我们要监听发送到 的消息的输出 :jms:outputspring-doc.cn

Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000)

然后,收到的消息将传递以下断言:spring-doc.cn

receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
receivedMessage.in.headers.get('BOOK-NAME') == 'foo'
场景 2(输出由输入触发)

由于已为您设置了路由,因此您可以向目标发送消息。jms:outputspring-doc.cn

producerTemplate.
        sendBodyAndHeaders('jms:input', new BookReturned('foo'), [sample: 'header'])

接下来,我们要监听发送到 的消息的输出,如下所示:jms:outputspring-doc.cn

Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000)

收到的消息将传递以下断言:spring-doc.cn

receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
receivedMessage.in.headers.get('BOOK-NAME') == 'foo'
场景 3(输入无输出)

由于已为您设置了路由,因此您可以向目标发送消息,如下所示:jms:outputspring-doc.cn

producerTemplate.
        sendBodyAndHeaders('jms:delete', new BookReturned('foo'), [sample: 'header'])

4.6. 使用 Spring 集成的消费者端消息传递

Spring Cloud Contract Stub Runner 的消息传递模块为您提供了一种简单的方法 与 Spring 集成。对于提供的工件,它会自动下载 存根并注册所需的路由。spring-doc.cn

4.6.1. 将 Runner 添加到项目中

您可以在 Spring Integration 和 Spring Cloud Contract Stub Runner 上同时拥有 classpath 的请记住使用 .@AutoConfigureStubRunnerspring-doc.cn

4.6.2. 禁用该功能

如果需要禁用此功能,请设置stubrunner.integration.enabled=falsespring-doc.cn

4.6.3. 示例

假设您有以下 Maven 存储库,其中包含为应用程序部署的存根:integrationServicespring-doc.cn

└── .m2
    └── repository
        └── io
            └── codearte
                └── accurest
                    └── stubs
                        └── integrationService
                            ├── 0.0.1-SNAPSHOT
                            │   ├── integrationService-0.0.1-SNAPSHOT.pom
                            │   ├── integrationService-0.0.1-SNAPSHOT-stubs.jar
                            │   └── maven-metadata-local.xml
                            └── maven-metadata-local.xml

进一步假设存根包含以下结构:spring-doc.cn

├── META-INF
│   └── MANIFEST.MF
└── repository
    ├── accurest
    │   ├── bookDeleted.groovy
    │   ├── bookReturned1.groovy
    │   └── bookReturned2.groovy
    └── mappings

考虑以下合约(编号 1 和 2):spring-doc.cn

Contract.make {
    label 'return_book_1'
    input {
        triggeredBy('bookReturnedTriggered()')
    }
    outputMessage {
        sentTo('output')
        body('''{ "bookName" : "foo" }''')
        headers {
            header('BOOK-NAME', 'foo')
        }
    }
}
Contract.make {
    label 'return_book_2'
    input {
        messageFrom('input')
        messageBody([
                bookName: 'foo'
        ])
        messageHeaders {
            header('sample', 'header')
        }
    }
    outputMessage {
        sentTo('output')
        body([
                bookName: 'foo'
        ])
        headers {
            header('BOOK-NAME', 'foo')
        }
    }
}

现在考虑以下 Spring 集成路由:spring-doc.cn

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns="http://www.springframework.org/schema/integration"
             xsi:schemaLocation="http://www.springframework.org/schema/beans
            https://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/integration
            http://www.springframework.org/schema/integration/spring-integration.xsd">


    <!-- REQUIRED FOR TESTING -->
    <bridge input-channel="output"
            output-channel="outputTest"/>

    <channel id="outputTest">
        <queue/>
    </channel>

</beans:beans>

这些示例适用于三种情况:spring-doc.cn

场景 1 (无输入消息)

要从标签触发消息,请使用接口,如 遵循:return_book_1StubTriggerspring-doc.cn

stubFinder.trigger('return_book_1')

以下清单显示了如何侦听 message sent to 的输出:jms:outputspring-doc.cn

Message<?> receivedMessage = messaging.receive('outputTest')

收到的消息将传递以下断言:spring-doc.cn

receivedMessage != null
assertJsons(receivedMessage.payload)
receivedMessage.headers.get('BOOK-NAME') == 'foo'
场景 2(输出由输入触发)

由于已为您设置了路由,因此您可以向目标发送消息,如下所示:jms:outputspring-doc.cn

messaging.send(new BookReturned('foo'), [sample: 'header'], 'input')

以下清单显示了如何侦听 message sent to 的输出:jms:outputspring-doc.cn

Message<?> receivedMessage = messaging.receive('outputTest')

收到的消息将传递以下断言:spring-doc.cn

receivedMessage != null
assertJsons(receivedMessage.payload)
receivedMessage.headers.get('BOOK-NAME') == 'foo'
场景 3(输入无输出)

由于已为您设置了路由,因此您可以向目标发送消息,如下所示:jms:inputspring-doc.cn

messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')

4.7. 使用 Spring Cloud Stream 进行消费者端消息传递

Spring Cloud Contract Stub Runner 的消息传递模块为您提供了一种简单的方法 与 Spring Stream 集成。对于提供的工件,它会自动下载 stubs 并注册所需的路由。spring-doc.cn

如果 Stub Runner 与 Stream 或字符串的集成 首先解析为 channel 的 ,并且不存在 destination 解析为通道名称。messageFromsentTodestinationdestination

如果要使用 Spring Cloud Stream,请记得添加对 test support 的依赖,如下所示:org.springframework.cloud:spring-cloud-streamspring-doc.cn

Maven 系列
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream</artifactId>
    <type>test-jar</type>
    <scope>test</scope>
    <classifier>test-binder</classifier>
</dependency>
Gradle
testImplementation(group: 'org.springframework.cloud', name: 'spring-cloud-stream', classifier: 'test-binder')

4.7.1. 将 Runner 添加到项目中

您可以在 Spring Cloud Stream 和 Spring Cloud Contract Stub Runner 上同时拥有 classpath 的请记住使用 .@AutoConfigureStubRunnerspring-doc.cn

4.7.2. 禁用该功能

如果需要禁用此功能,请设置stubrunner.stream.enabled=falsespring-doc.cn

4.7.3. 示例

假设您有以下 Maven 存储库,其中包含为应用程序部署的存根:streamServicespring-doc.cn

└── .m2
    └── repository
        └── io
            └── codearte
                └── accurest
                    └── stubs
                        └── streamService
                            ├── 0.0.1-SNAPSHOT
                            │   ├── streamService-0.0.1-SNAPSHOT.pom
                            │   ├── streamService-0.0.1-SNAPSHOT-stubs.jar
                            │   └── maven-metadata-local.xml
                            └── maven-metadata-local.xml

进一步假设存根包含以下结构:spring-doc.cn

├── META-INF
│   └── MANIFEST.MF
└── repository
    ├── accurest
    │   ├── bookDeleted.groovy
    │   ├── bookReturned1.groovy
    │   └── bookReturned2.groovy
    └── mappings

考虑以下合约(编号 1 和 2):spring-doc.cn

Contract.make {
    label 'return_book_1'
    input { triggeredBy('bookReturnedTriggered()') }
    outputMessage {
        sentTo('returnBook')
        body('''{ "bookName" : "foo" }''')
        headers { header('BOOK-NAME', 'foo') }
    }
}
Contract.make {
    label 'return_book_2'
    input {
        messageFrom('bookStorage')
        messageBody([
                bookName: 'foo'
        ])
        messageHeaders { header('sample', 'header') }
    }
    outputMessage {
        sentTo('returnBook')
        body([
                bookName: 'foo'
        ])
        headers { header('BOOK-NAME', 'foo') }
    }
}

现在考虑以下 Spring 配置:spring-doc.cn

stubrunner.repositoryRoot: classpath:m2repo/repository/
stubrunner.ids: org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs
stubrunner.stubs-mode: remote
spring:
  cloud:
    stream:
      bindings:
        output:
          destination: returnBook
        input:
          destination: bookStorage

server:
  port: 0

debug: true

这些示例适用于三种情况:spring-doc.cn

场景 1 (无输入消息)

要从标签触发消息,请使用 遵循:return_book_1StubTriggerspring-doc.cn

stubFinder.trigger('return_book_1')

以下示例说明如何侦听发送到通道的消息的输出,该通道为 :destinationreturnBookspring-doc.cn

Message<?> receivedMessage = messaging.receive('returnBook')

收到的消息将传递以下断言:spring-doc.cn

receivedMessage != null
assertJsons(receivedMessage.payload)
receivedMessage.headers.get('BOOK-NAME') == 'foo'
场景 2(输出由输入触发)

由于路由已为您设置,因此您可以向 发送消息,如下所示:bookStoragedestinationspring-doc.cn

messaging.send(new BookReturned('foo'), [sample: 'header'], 'bookStorage')

以下示例显示如何侦听发送到 的消息的输出 :returnBookspring-doc.cn

Message<?> receivedMessage = messaging.receive('returnBook')

收到的消息将传递以下断言:spring-doc.cn

receivedMessage != null
assertJsons(receivedMessage.payload)
receivedMessage.headers.get('BOOK-NAME') == 'foo'
场景 3(输入无输出)

由于已为您设置了路由,因此您可以向目标发送消息,如下所示:jms:outputspring-doc.cn

messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')

4.8. 使用 Spring AMQP 进行消费者端消息传递

Spring Cloud Contract Stub Runner 的消息传递模块提供了一种简单的方法 与 Spring AMQP 的 Rabbit Template 集成。对于提供的工件,它 自动下载存根并注册所需的路由。spring-doc.cn

集成尝试独立工作(即,不与正在运行的 RabbitMQ 消息代理)。它期望在应用程序上下文中有一个 将其用作名为 的 Spring Boot 测试。因此,它可以使用 Mockito 间谍 验证和检查应用程序发送的消息的功能。RabbitTemplate@SpyBeanspring-doc.cn

在消息使用者端,存根运行程序将所有 -annotated endpoints 和 application context 上的所有对象。@RabbitListenerSimpleMessageListenerContainerspring-doc.cn

由于消息通常发送到 AMQP 中的交易所,因此消息协定包含 exchange name 作为目标。另一端的消息侦听器绑定到 队列。绑定将 exchange 连接到队列。如果触发了消息协定,则 Spring AMQP 存根运行程序集成在应用程序上下文上查找绑定 匹配此交换。然后,它从 Spring 交换中收集队列并尝试 查找绑定到这些队列的消息侦听器。系统会为所有匹配项触发该消息 消息侦听器。spring-doc.cn

如果需要使用路由键,可以使用消息标头传递路由键。amqp_receivedRoutingKeyspring-doc.cn

4.8.1. 将 Runner 添加到项目中

你可以在 Classpath 上同时拥有 Spring AMQP 和 Spring Cloud Contract Stub Runner,并且 设置属性 .请记住为您的测试类添加注释 跟。stubrunner.amqp.enabled=true@AutoConfigureStubRunnerspring-doc.cn

如果 Classpath 上已经有 Stream 和 Integration,则需要 通过设置 AND 属性来显式禁用它们。stubrunner.stream.enabled=falsestubrunner.integration.enabled=false

4.8.2. 示例

假设您有以下 Maven 存储库,其中包含为应用程序部署的存根:spring-cloud-contract-amqp-testspring-doc.cn

└── .m2
    └── repository
        └── com
            └── example
                └── spring-cloud-contract-amqp-test
                    ├── 0.4.0-SNAPSHOT
                    │   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT.pom
                    │   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT-stubs.jar
                    │   └── maven-metadata-local.xml
                    └── maven-metadata-local.xml

进一步假设存根包含以下结构:spring-doc.cn

├── META-INF
│   └── MANIFEST.MF
└── contracts
    └── shouldProduceValidPersonData.groovy

然后考虑以下合约:spring-doc.cn

Contract.make {
    // Human readable description
    description 'Should produce valid person data'
    // Label by means of which the output message can be triggered
    label 'contract-test.person.created.event'
    // input to the contract
    input {
        // the contract will be triggered by a method
        triggeredBy('createPerson()')
    }
    // output message of the contract
    outputMessage {
        // destination to which the output message will be sent
        sentTo 'contract-test.exchange'
        headers {
            header('contentType': 'application/json')
            header('__TypeId__': 'org.springframework.cloud.contract.stubrunner.messaging.amqp.Person')
        }
        // the body of the output message
        body([
                id  : $(consumer(9), producer(regex("[0-9]+"))),
                name: "me"
        ])
    }
}

现在考虑以下 Spring 配置:spring-doc.cn

stubrunner:
  repositoryRoot: classpath:m2repo/repository/
  ids: org.springframework.cloud.contract.verifier.stubs.amqp:spring-cloud-contract-amqp-test:0.4.0-SNAPSHOT:stubs
  stubs-mode: remote
  amqp:
    enabled: true
server:
  port: 0
触发消息

要使用上一节中的协定触发消息,请使用接口,因为 遵循:StubTriggerspring-doc.cn

stubTrigger.trigger("contract-test.person.created.event")

该消息的目标为 ,因此 Spring AMQP 存根运行程序 Integration 会查找与此 Exchange 相关的绑定,如下例所示:contract-test.exchangespring-doc.cn

@Bean
public Binding binding() {
    return BindingBuilder.bind(new Queue("test.queue")).to(new DirectExchange("contract-test.exchange"))
            .with("#");
}

绑定定义绑定名为 .因此,以下侦听器 定义与 Contract 消息匹配并调用:test.queuespring-doc.cn

@Bean
public SimpleMessageListenerContainer simpleMessageListenerContainer(ConnectionFactory connectionFactory,
        MessageListenerAdapter listenerAdapter) {
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("test.queue");
    container.setMessageListener(listenerAdapter);

    return container;
}

此外,以下带注释的侦听器匹配并被调用:spring-doc.cn

@RabbitListener(bindings = @QueueBinding(value = @Queue("test.queue"),
        exchange = @Exchange(value = "contract-test.exchange", ignoreDeclarationExceptions = "true")))
public void handlePerson(Person person) {
    this.person = person;
}
该消息直接交给与匹配的 关联的 方法 。onMessageMessageListenerSimpleMessageListenerContainer
Spring AMQP 测试配置

为了避免 Spring AMQP 在我们的测试期间尝试连接到正在运行的代理,我们 配置 mock 。ConnectionFactoryspring-doc.cn

要禁用 mocked ,请设置以下属性: ,如下所示:ConnectionFactorystubrunner.amqp.mockConnection=falsespring-doc.cn

stubrunner:
  amqp:
    mockConnection: false

4.9. 使用 Spring JMS 进行消费者端消息传递

Spring Cloud Contract Stub Runner 的消息传递模块提供了一种简单的方法 与 Spring JMS 集成。spring-doc.cn

该集成假定您有一个正在运行的 JMS 代理实例(例如嵌入式代理)。activemqspring-doc.cn

4.9.1. 将 Runner 添加到项目中

您需要在 Classpath 上同时具有 Spring JMS 和 Spring Cloud Contract Stub Runner。请记住为您的测试类添加注释 跟。@AutoConfigureStubRunnerspring-doc.cn

4.9.2. 示例

假设存根结构如下所示:spring-doc.cn

├── stubs
    ├── bookDeleted.groovy
    ├── bookReturned1.groovy
    └── bookReturned2.groovy

进一步假设以下测试配置:spring-doc.cn

stubrunner:
  repository-root: stubs:classpath:/stubs/
  ids: my:stubs
  stubs-mode: remote
spring:
  activemq:
    send-timeout: 1000
  jms:
    template:
      receive-timeout: 1000

现在考虑以下合约(我们给它们编号 1 和 2):spring-doc.cn

Contract.make {
    label 'return_book_1'
    input {
        triggeredBy('bookReturnedTriggered()')
    }
    outputMessage {
        sentTo('output')
        body('''{ "bookName" : "foo" }''')
        headers {
            header('BOOK-NAME', 'foo')
        }
    }
}
Contract.make {
    label 'return_book_2'
    input {
        messageFrom('input')
        messageBody([
                bookName: 'foo'
        ])
        messageHeaders {
            header('sample', 'header')
        }
    }
    outputMessage {
        sentTo('output')
        body([
                bookName: 'foo'
        ])
        headers {
            header('BOOK-NAME', 'foo')
        }
    }
}
场景 1 (无输入消息)

要从标签触发消息,我们使用接口,如下所示:return_book_1StubTriggerspring-doc.cn

stubFinder.trigger('return_book_1')

接下来,我们要监听发送到 的消息的输出 :outputspring-doc.cn

TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')

然后,收到的消息将传递以下断言:spring-doc.cn

receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.getText())
receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
场景 2(输出由输入触发)

由于已为您设置了路由,因此您可以向目标发送消息。outputspring-doc.cn

jmsTemplate.
        convertAndSend('input', new BookReturned('foo'), new MessagePostProcessor() {
            @Override
            Message postProcessMessage(Message message) throws JMSException {
                message.setStringProperty("sample", "header")
                return message
            }
        })

接下来,我们要监听发送到 的消息的输出,如下所示:outputspring-doc.cn

TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')

收到的消息将传递以下断言:spring-doc.cn

receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.getText())
receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
场景 3(输入无输出)

由于已为您设置了路由,因此您可以向目标发送消息,如下所示:outputspring-doc.cn

jmsTemplate.
        convertAndSend('delete', new BookReturned('foo'), new MessagePostProcessor() {
            @Override
            Message postProcessMessage(Message message) throws JMSException {
                message.setStringProperty("sample", "header")
                return message
            }
        })

4.10. 使用 Spring Kafka 进行消费者端消息传递

Spring Cloud Contract Stub Runner 的消息传递模块提供了一种简单的方法 与 Spring Kafka 集成。spring-doc.cn

该集成假定您有一个正在运行的嵌入式 Kafka 代理实例(通过依赖项)。spring-kafka-testspring-doc.cn

4.10.1. 将 Runner 添加到项目中

您需要在 Classpath 上具有 Spring Kafka、Spring Kafka Test(要运行 )和 Spring Cloud Contract Stub Runner。请记住为您的测试类添加注释 跟。@EmbeddedBroker@AutoConfigureStubRunnerspring-doc.cn

使用 Kafka 集成,为了轮询单个消息,我们需要在 Spring 上下文启动时注册一个使用者。这可能会导致这样一种情况:当您在使用者端时,Stub Runner 可以为相同的组 ID 和主题注册额外的使用者。这可能会导致只有一个组件实际轮询消息的情况。由于在消费者方面,您同时拥有 Spring Cloud Contract Stub Runner 和 Spring Cloud Contract Verifier Classpath,因此我们需要能够关闭此类行为。这是通过标志自动完成的,该标志将禁用 Contact Verifier 使用者注册。如果您的应用程序既是 Kafka 消息的使用者又是生成者的创建者,则可能需要在生成的测试的基类中手动将该属性切换到。stubrunner.kafka.initializer.enabledfalsespring-doc.cn

如果您有多个 bean,则可以提供自己的 bean 类型,该 bean 将返回您选择的 bean。KafkaTemplateSupplier<KafkaTemplate>KafkaTemplatespring-doc.cn

4.10.2. 示例

假设存根结构如下所示:spring-doc.cn

├── stubs
    ├── bookDeleted.groovy
    ├── bookReturned1.groovy
    └── bookReturned2.groovy

进一步假设以下测试配置(注意指向嵌入式代理的 IP via ):spring.kafka.bootstrap-servers${spring.embedded.kafka.brokers}spring-doc.cn

stubrunner:
  repository-root: stubs:classpath:/stubs/
  ids: my:stubs
  stubs-mode: remote
spring:
  kafka:
    bootstrap-servers: ${spring.embedded.kafka.brokers}
    producer:
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
      properties:
        "spring.json.trusted.packages": "*"
    consumer:
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        "spring.json.trusted.packages": "*"
      group-id: groupId
如果您的应用程序使用非整数记录键,则需要相应地设置 and 属性,因为 Kafka 反序列化需要非 null record keys 设置为 integer 类型。spring.kafka.producer.key-serializerspring.kafka.consumer.key-deserializer

现在考虑以下合约(我们给它们编号 1 和 2):spring-doc.cn

Contract.make {
    label 'return_book_1'
    input {
        triggeredBy('bookReturnedTriggered()')
    }
    outputMessage {
        sentTo('output')
        body('''{ "bookName" : "foo" }''')
        headers {
            header('BOOK-NAME', 'foo')
        }
    }
}
Contract.make {
    label 'return_book_2'
    input {
        messageFrom('input')
        messageBody([
                bookName: 'foo'
        ])
        messageHeaders {
            header('sample', 'header')
        }
    }
    outputMessage {
        sentTo('output')
        body([
                bookName: 'foo'
        ])
        headers {
            header('BOOK-NAME', 'foo')
        }
    }
}
场景 1 (无输入消息)

要从标签触发消息,我们使用接口,如下所示:return_book_1StubTriggerspring-doc.cn

stubFinder.trigger('return_book_1')

接下来,我们要监听发送到 的消息的输出 :outputspring-doc.cn

Message receivedMessage = receiveFromOutput()

然后,收到的消息将传递以下断言:spring-doc.cn

assert receivedMessage != null
assert assertThatBodyContainsBookNameFoo(receivedMessage.getPayload())
assert receivedMessage.getHeaders().get('BOOK-NAME') == 'foo'
场景 2(输出由输入触发)

由于已为您设置了路由,因此您可以向目标发送消息。outputspring-doc.cn

Message message = MessageBuilder.createMessage(new BookReturned('foo'), new MessageHeaders([sample: "header",]))
kafkaTemplate.setDefaultTopic('input')
kafkaTemplate.send(message)
Message message = MessageBuilder.createMessage(new BookReturned('bar'), new MessageHeaders([kafka_messageKey: "bar5150",]))
kafkaTemplate.setDefaultTopic('input2')
kafkaTemplate.send(message)

接下来,我们要监听发送到 的消息的输出,如下所示:outputspring-doc.cn

Message receivedMessage = receiveFromOutput()
Message receivedMessage = receiveFromOutput()

收到的消息将传递以下断言:spring-doc.cn

assert receivedMessage != null
assert assertThatBodyContainsBookNameFoo(receivedMessage.getPayload())
assert receivedMessage.getHeaders().get('BOOK-NAME') == 'foo'
assert receivedMessage != null
assert assertThatBodyContainsBookName(receivedMessage.getPayload(), 'bar')
assert receivedMessage.getHeaders().get('BOOK-NAME') == 'bar'
assert receivedMessage.getHeaders().get("kafka_receivedMessageKey") == 'bar5150'
场景 3(输入无输出)

由于已为您设置了路由,因此您可以向目标发送消息,如下所示:outputspring-doc.cn

Message message = MessageBuilder.createMessage(new BookReturned('foo'), new MessageHeaders([sample: "header",]))
kafkaTemplate.setDefaultTopic('delete')
kafkaTemplate.send(message)

5. Spring Cloud Contract Stub 运行器

您在使用 Spring Cloud Contract Verifier 时可能会遇到的问题之一是 将生成的 WireMock JSON 存根从服务器端传递到客户端(或传递给 各种客户端)。在消息传递的客户端生成方面也是如此。spring-doc.cn

复制 JSON 文件并手动设置客户端消息收发不在 问题。这就是我们引入 Spring Cloud Contract Stub Runner 的原因。它可以 自动下载并运行存根。spring-doc.cn

5.1. 快照版本

您可以将其他快照存储库添加到构建文件中以使用 snapshot 版本,这些版本在每次成功构建后自动上传,如下所示:spring-doc.cn

Maven 系列
<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <!--<repository>
        <id>spring-releases</id>
        <name>Spring Releases</name>
        <url>https://repo.spring.io/release</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>-->
</repositories>
<pluginRepositories>
    <pluginRepository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </pluginRepository>
    <pluginRepository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </pluginRepository>
    <!--<pluginRepository>
        <id>spring-releases</id>
        <name>Spring Releases</name>
        <url>https://repo.spring.io/release</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </pluginRepository>-->
</pluginRepositories>
Gradle (settings.xml)
pluginManagement {
    repositories {
        mavenLocal()
        maven { url "https://repo.spring.io/snapshot" }
        maven { url "https://repo.spring.io/milestone" }
        maven { url "https://repo.spring.io/release" }
        gradlePluginPortal()
    }

5.2. 将存根发布为 JAR

将存根发布为 jar 的最简单方法是集中保存存根的方式。 例如,您可以将它们作为 jar 保存在 Maven 存储库中。spring-doc.cn

对于 Maven 和 Gradle,设置已准备就绪。但是,您可以自定义 如果你愿意,就去吧。

以下示例演示如何将存根发布为 jar:spring-doc.cn

Maven 系列
<!-- First disable the default jar setup in the properties section -->
<!-- we don't want the verifier to do a jar for us -->
<spring.cloud.contract.verifier.skip>true</spring.cloud.contract.verifier.skip>

<!-- Next add the assembly plugin to your build -->
<!-- we want the assembly plugin to generate the JAR -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <executions>
        <execution>
            <id>stub</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>single</goal>
            </goals>
            <inherited>false</inherited>
            <configuration>
                <attach>true</attach>
                <descriptors>
                    $/tmp/releaser-1706094216595-0/spring-cloud-contract/docs/src/assembly/stub.xml
                </descriptors>
            </configuration>
        </execution>
    </executions>
</plugin>

<!-- Finally setup your assembly. Below you can find the contents of src/main/assembly/stub.xml -->
<assembly
    xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd">
    <id>stubs</id>
    <formats>
        <format>jar</format>
    </formats>
    <includeBaseDirectory>false</includeBaseDirectory>
    <fileSets>
        <fileSet>
            <directory>src/main/java</directory>
            <outputDirectory>/</outputDirectory>
            <includes>
                <include>**com/example/model/*.*</include>
            </includes>
        </fileSet>
        <fileSet>
            <directory>${project.build.directory}/classes</directory>
            <outputDirectory>/</outputDirectory>
            <includes>
                <include>**com/example/model/*.*</include>
            </includes>
        </fileSet>
        <fileSet>
            <directory>${project.build.directory}/snippets/stubs</directory>
            <outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/mappings</outputDirectory>
            <includes>
                <include>**/*</include>
            </includes>
        </fileSet>
        <fileSet>
            <directory>$/tmp/releaser-1706094216595-0/spring-cloud-contract/docs/src/test/resources/contracts</directory>
            <outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/contracts</outputDirectory>
            <includes>
                <include>**/*.groovy</include>
            </includes>
        </fileSet>
    </fileSets>
</assembly>
Gradle
ext {
    contractsDir = file("mappings")
    stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/")
}

// Automatically added by plugin:
// copyContracts - copies contracts to the output folder from which JAR will be created
// verifierStubsJar - JAR with a provided stub suffix

publishing {
    publications {
        stubs(MavenPublication) {
            artifactId "${project.name}-stubs"
            artifact verifierStubsJar
        }
    }
}

5.3. Stub Runner 核心

存根运行器核心为服务协作者运行存根。将存根视为 services 允许您使用 stub-runner 作为 Consumer 驱动的 Contract 的实现。spring-doc.cn

Stub Runner 允许您自动下载提供的依赖项的存根(或 从 Classpath 中选择它们),为它们启动 WireMock 服务器,并为它们提供适当的 存根定义。对于消息传递,定义了特殊的存根路由。spring-doc.cn

5.3.1. 检索存根

您可以从以下获取存根的选项中进行选择:spring-doc.cn

  • 基于 Aether 的解决方案,可从 Artifactory 或 Nexus 下载带有存根的 JARspring-doc.cn

  • Classpath-scanning 解决方案,它使用模式搜索 Classpath 以检索存根spring-doc.cn

  • 编写您自己的 实现以实现完全自定义org.springframework.cloud.contract.stubrunner.StubDownloaderBuilderspring-doc.cn

后一个示例在 Custom Stub Runner 部分中介绍。spring-doc.cn

下载 Stub

您可以使用开关控制存根的下载。它从枚举中选取值。您可以使用以下选项:stubsModeStubRunnerProperties.StubsModespring-doc.cn

  • StubRunnerProperties.StubsMode.CLASSPATH(默认值):从 Classpath 中选择存根spring-doc.cn

  • StubRunnerProperties.StubsMode.LOCAL:从本地存储中选取存根(例如.m2)spring-doc.cn

  • StubRunnerProperties.StubsMode.REMOTE:从远程位置选取存根spring-doc.cn

以下示例从本地位置选取存根:spring-doc.cn

@AutoConfigureStubRunner(repositoryRoot="https://foo.bar", ids = "com.example:beer-api-producer:+:stubs:8095", stubsMode = StubRunnerProperties.StubsMode.LOCAL)
类路径扫描

如果将属性设置为(或未设置任何内容,因为是默认值),则会扫描 Classpath。 请考虑以下示例:stubsModeStubRunnerProperties.StubsMode.CLASSPATHCLASSPATHspring-doc.cn

@AutoConfigureStubRunner(ids = {
    "com.example:beer-api-producer:+:stubs:8095",
    "com.example.foo:bar:1.0.0:superstubs:8096"
})

您可以将依赖项添加到 Classpath 中,如下所示:spring-doc.cn

Maven 系列
<dependency>
    <groupId>com.example</groupId>
    <artifactId>beer-api-producer-restdocs</artifactId>
    <classifier>stubs</classifier>
    <version>0.0.1-SNAPSHOT</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>*</groupId>
            <artifactId>*</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.example.thing1</groupId>
    <artifactId>thing2</artifactId>
    <classifier>superstubs</classifier>
    <version>1.0.0</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>*</groupId>
            <artifactId>*</artifactId>
        </exclusion>
    </exclusions>
</dependency>
Gradle
testCompile("com.example:beer-api-producer-restdocs:0.0.1-SNAPSHOT:stubs") {
    transitive = false
}
testCompile("com.example.thing1:thing2:1.0.0:superstubs") {
    transitive = false
}

然后扫描 Classpath 上的指定位置。为 将扫描以下位置:com.example:beer-api-producer-restdocsspring-doc.cn

  • /META-INF/com.example/beer-api-producer-restdocs/*/.*spring-doc.cn

  • /contracts/com.example/beer-api-producer-restdocs/*/.*spring-doc.cn

  • /mappings/com.example/beer-api-producer-restdocs/*/.*spring-doc.cn

对于 ,将扫描以下位置:com.example.thing1:thing2spring-doc.cn

在打包 producer stub 的

为了实现正确的存根打包,生产者将按如下方式设置 Contract:spring-doc.cn

└── src
    └── test
        └── resources
            └── contracts
                └── com.example
                    └── beer-api-producer-restdocs
                        └── nested
                            └── contract3.groovy

通过使用 Maven 程序集插件Gradle Jar 任务,您必须创建以下内容 结构中:spring-doc.cn

└── META-INF
    └── com.example
        └── beer-api-producer-restdocs
            └── 2.0.0
                ├── contracts
                │   └── nested
                │       └── contract2.groovy
                └── mappings
                    └── mapping.json

通过维护此结构,可以扫描 Classpath,您可以从消息传递或 HTTP 存根,而无需下载构件。spring-doc.cn

配置 HTTP 服务器存根

Stub Runner 有一个 a 的概念,它抽象了底层 HTTP 服务器的具体实现(例如,WireMock 是其中一种实现)。 有时,您需要对存根服务器执行一些额外的调整(这对于给定的实现是具体的)。 为此,Stub Runner 为您提供 注解中可用的属性和 JUnit 规则,可通过系统属性进行访问,您可以在其中提供 您的接口实现。实现可以改变 给定 HTTP 服务器存根的配置文件。HttpServerStubhttpServerStubConfigurerorg.springframework.cloud.contract.stubrunner.HttpServerStubConfigurerspring-doc.cn

Spring Cloud Contract Stub Runner 附带一个实现,您可以 可以扩展 WireMock: . 在该方法中, 您可以为给定的存根提供自己的自定义配置。用途 case 可能会在 HTTPS 端口上为给定的项目 ID 启动 WireMock。以下内容 示例展示了如何做到这一点:org.springframework.cloud.contract.stubrunner.provider.wiremock.WireMockHttpServerStubConfigurerconfigurespring-doc.cn

示例 1.WireMockHttpServerStubConfigurer 实现
@CompileStatic
static class HttpsForFraudDetection extends WireMockHttpServerStubConfigurer {

    private static final Log log = LogFactory.getLog(HttpsForFraudDetection)

    @Override
    WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) {
        if (httpServerStubConfiguration.stubConfiguration.artifactId == "fraudDetectionServer") {
            int httpsPort = SocketUtils.findAvailableTcpPort()
            log.info("Will set HTTPs port [" + httpsPort + "] for fraud detection server")
            return httpStubConfiguration
                    .httpsPort(httpsPort)
        }
        return httpStubConfiguration
    }
}

然后,您可以将其与注释一起重复使用,如下所示:@AutoConfigureStubRunnerspring-doc.cn

@AutoConfigureStubRunner(mappingsOutputFolder = "target/outputmappings/",
        httpServerStubConfigurer = HttpsForFraudDetection)

每当找到 HTTPS 端口时,它优先于 HTTP 端口。spring-doc.cn

5.3.2. 运行 stub

本节介绍如何执行 stub。它包含以下主题:spring-doc.cn

HTTP 存根

存根在 JSON 文档中定义,其语法在 WireMock 文档中定义。spring-doc.cn

以下示例在 JSON 中定义一个存根:spring-doc.cn

{
    "request": {
        "method": "GET",
        "url": "/ping"
    },
    "response": {
        "status": 200,
        "body": "pong",
        "headers": {
            "Content-Type": "text/plain"
        }
    }
}
查看已注册的映射

每个存根协作者都会在终端节点下公开一个已定义映射的列表。__/admin/spring-doc.cn

您还可以使用该属性将映射转储到文件。 对于基于注释的方法,它类似于以下示例:mappingsOutputFolderspring-doc.cn

@AutoConfigureStubRunner(ids="a.b.c:loanIssuance,a.b.c:fraudDetectionServer",
mappingsOutputFolder = "target/outputmappings/")

对于 JUnit 方法,它类似于以下示例:spring-doc.cn

@ClassRule @Shared StubRunnerRule rule = new StubRunnerRule()
            .repoRoot("https://some_url")
            .downloadStub("a.b.c", "loanIssuance")
            .downloadStub("a.b.c:fraudDetectionServer")
            .withMappingsOutputFolder("target/outputmappings")

然后,如果您签出文件夹,您将看到以下结构;target/outputmappingsspring-doc.cn

.
├── fraudDetectionServer_13705
└── loanIssuance_12255

这意味着注册了两个存根。 已在 port 和 port 注册。如果我们查看其中一个文件,我们将看到(对于 WireMock) 可用于给定服务器的映射:fraudDetectionServer13705loanIssuance12255spring-doc.cn

[{
  "id" : "f9152eb9-bf77-4c38-8289-90be7d10d0d7",
  "request" : {
    "url" : "/name",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "body" : "fraudDetectionServer"
  },
  "uuid" : "f9152eb9-bf77-4c38-8289-90be7d10d0d7"
},
...
]
消息存根

根据提供的 Stub Runner 依赖项和 DSL,消息收发路由会自动设置。spring-doc.cn

5.4. Stub Runner JUnit 规则和 Stub Runner JUnit5 扩展

Stub Runner 附带一个 JUnit 规则,允许您下载和运行给定的 stub group 和工件 ID,如下例所示:spring-doc.cn

@ClassRule
public static StubRunnerRule rule = new StubRunnerRule().repoRoot(repoRoot())
        .stubsMode(StubRunnerProperties.StubsMode.REMOTE)
        .downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")
        .downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer");

@BeforeClass
@AfterClass
public static void setupProps() {
    System.clearProperty("stubrunner.repository.root");
    System.clearProperty("stubrunner.classifier");
}

A 也可用于 JUnit 5。 并以非常相似的方式工作。在规则或扩展名 调用,则 Stub Runner 会连接到您的 Maven 存储库,并且对于给定的 依赖项尝试:StubRunnerExtensionStubRunnerRuleStubRunnerExtensionspring-doc.cn

  • 下载spring-doc.cn

  • 在本地缓存它们spring-doc.cn

  • 将它们解压缩到临时文件夹spring-doc.cn

  • 从提供的随机端口为每个 Maven 依赖项启动一个 WireMock 服务器 端口范围或提供的端口spring-doc.cn

  • 向 WireMock 服务器提供所有作为有效 WireMock 定义的 JSON 文件spring-doc.cn

  • 发送消息(记得传递 interface 的实现)MessageVerifierspring-doc.cn

Stub Runner 使用 Eclipse Aether 机制下载 Maven 依赖项。 查看他们的文档以获取更多信息。spring-doc.cn

由于 和 实现 ,他们让 您可以找到 Started Stubs,如下例所示:StubRunnerRuleStubRunnerExtensionStubFinderspring-doc.cn

package org.springframework.cloud.contract.stubrunner;

import java.net.URL;
import java.util.Collection;
import java.util.Map;

import org.springframework.cloud.contract.spec.Contract;

/**
 * Contract for finding registered stubs.
 *
 * @author Marcin Grzejszczak
 */
public interface StubFinder extends StubTrigger {

    /**
     * For the given groupId and artifactId tries to find the matching URL of the running
     * stub.
     * @param groupId - might be null. In that case a search only via artifactId takes
     * place
     * @param artifactId - artifact id of the stub
     * @return URL of a running stub or throws exception if not found
     * @throws StubNotFoundException in case of not finding a stub
     */
    URL findStubUrl(String groupId, String artifactId) throws StubNotFoundException;

    /**
     * For the given Ivy notation {@code [groupId]:artifactId:[version]:[classifier]}
     * tries to find the matching URL of the running stub. You can also pass only
     * {@code artifactId}.
     * @param ivyNotation - Ivy representation of the Maven artifact
     * @return URL of a running stub or throws exception if not found
     * @throws StubNotFoundException in case of not finding a stub
     */
    URL findStubUrl(String ivyNotation) throws StubNotFoundException;

    /**
     * @return all running stubs
     */
    RunningStubs findAllRunningStubs();

    /**
     * @return the list of Contracts
     */
    Map<StubConfiguration, Collection<Contract>> getContracts();

}

以下示例提供了有关使用 Stub Runner 的更多详细信息:spring-doc.cn

斯波克
@ClassRule
@Shared
StubRunnerRule rule = new StubRunnerRule()
        .stubsMode(StubRunnerProperties.StubsMode.REMOTE)
        .repoRoot(StubRunnerRuleSpec.getResource("/m2repo/repository").toURI().toString())
        .downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")
        .downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")
        .withMappingsOutputFolder("target/outputmappingsforrule")


def 'should start WireMock servers'() {
    expect: 'WireMocks are running'
        rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null
        rule.findStubUrl('loanIssuance') != null
        rule.findStubUrl('loanIssuance') == rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance')
        rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null
    and:
        rule.findAllRunningStubs().isPresent('loanIssuance')
        rule.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer')
        rule.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer')
    and: 'Stubs were registered'
        "${rule.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
        "${rule.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
}

def 'should output mappings to output folder'() {
    when:
        def url = rule.findStubUrl('fraudDetectionServer')
    then:
        new File("target/outputmappingsforrule", "fraudDetectionServer_${url.port}").exists()
}
朱尼特 4
@Test
public void should_start_wiremock_servers() throws Exception {
    // expect: 'WireMocks are running'
    then(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")).isNotNull();
    then(rule.findStubUrl("loanIssuance")).isNotNull();
    then(rule.findStubUrl("loanIssuance"))
            .isEqualTo(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs", "loanIssuance"));
    then(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")).isNotNull();
    // and:
    then(rule.findAllRunningStubs().isPresent("loanIssuance")).isTrue();
    then(rule.findAllRunningStubs().isPresent("org.springframework.cloud.contract.verifier.stubs",
            "fraudDetectionServer")).isTrue();
    then(rule.findAllRunningStubs()
            .isPresent("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")).isTrue();
    // and: 'Stubs were registered'
    then(httpGet(rule.findStubUrl("loanIssuance").toString() + "/name")).isEqualTo("loanIssuance");
    then(httpGet(rule.findStubUrl("fraudDetectionServer").toString() + "/name")).isEqualTo("fraudDetectionServer");
}
朱尼特 5
// Visible for Junit
@RegisterExtension
static StubRunnerExtension stubRunnerExtension = new StubRunnerExtension().repoRoot(repoRoot())
        .stubsMode(StubRunnerProperties.StubsMode.REMOTE)
        .downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")
        .downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")
        .withMappingsOutputFolder("target/outputmappingsforrule");

@BeforeAll
@AfterAll
static void setupProps() {
    System.clearProperty("stubrunner.repository.root");
    System.clearProperty("stubrunner.classifier");
}

private static String repoRoot() {
    try {
        return StubRunnerRuleJUnitTest.class.getResource("/m2repo/repository/").toURI().toString();
    }
    catch (Exception e) {
        return "";
    }
}

有关更多信息,请参阅 JUnit 和 Spring 的通用属性 如何应用 Stub Runner 的全局配置。spring-doc.cn

要将 JUnit 规则或 JUnit 5 扩展与消息收发一起使用,您必须为规则生成器提供接口的实现(例如, )。 如果不这样做,则每当尝试发送消息时,都会引发异常。MessageVerifierrule.messageVerifier(new MyMessageVerifier())

5.4.1. Maven 设置

存根下载程序遵循其他本地存储库文件夹的 Maven 设置。 当前不考虑存储库和配置文件的身份验证详细信息。 因此,您需要使用上述属性来指定它。spring-doc.cn

5.4.2. 提供固定端口

您还可以在固定端口上运行存根。您可以通过两种不同的方式来实现。 一种是在 properties 中传递,另一种是使用 Fluent API JUnit 规则。spring-doc.cn

5.4.3. Fluent API

使用 or 时,您可以添加要下载的存根 然后传递最后下载的存根的端口。以下示例显示了如何执行此操作:StubRunnerRuleStubRunnerExtensionspring-doc.cn

@ClassRule
public static StubRunnerRule rule = new StubRunnerRule().repoRoot(repoRoot())
        .stubsMode(StubRunnerProperties.StubsMode.REMOTE)
        .downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance").withPort(35465)
        .downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer:35466");

@BeforeClass
@AfterClass
public static void setupProps() {
    System.clearProperty("stubrunner.repository.root");
    System.clearProperty("stubrunner.classifier");
}

对于前面的示例,以下测试有效:spring-doc.cn

then(rule.findStubUrl("loanIssuance")).isEqualTo(URI.create("http://localhost:35465").toURL());
then(rule.findStubUrl("fraudDetectionServer")).isEqualTo(URI.create("http://localhost:35466").toURL());

5.4.4. 带弹簧的短管 Runner

Stub Runner with Spring 设置 Stub Runner 项目的 Spring 配置。spring-doc.cn

通过在配置文件中提供存根列表,Stub Runner 会自动下载 并在 WireMock 中注册选定的存根。spring-doc.cn

如果你想找到你的存根依赖项的 URL,你可以自动装配接口并使用 其方法,如下所示:StubFinderspring-doc.cn

@SpringBootTest(classes = Config, properties = [" stubrunner.cloud.enabled=false",
        'foo=${stubrunner.runningstubs.fraudDetectionServer.port}',
        'fooWithGroup=${stubrunner.runningstubs.org.springframework.cloud.contract.verifier.stubs.fraudDetectionServer.port}'])
@AutoConfigureStubRunner(mappingsOutputFolder = "target/outputmappings/",
        httpServerStubConfigurer = HttpsForFraudDetection)
@ActiveProfiles("test")
class StubRunnerConfigurationSpec extends Specification {

    @Autowired
    StubFinder stubFinder
    @Autowired
    Environment environment
    @StubRunnerPort("fraudDetectionServer")
    int fraudDetectionServerPort
    @StubRunnerPort("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")
    int fraudDetectionServerPortWithGroupId
    @Value('${foo}')
    Integer foo

    void setupSpec() {
        System.clearProperty("stubrunner.repository.root")
        System.clearProperty("stubrunner.classifier")
        WireMockHttpServerStubAccessor.clear()
    }

    void cleanupSpec() {
        setupSpec()
    }

    def 'should mark all ports as random'() {
        expect:
        WireMockHttpServerStubAccessor.everyPortRandom()
    }

    def 'should start WireMock servers'() {
        expect: 'WireMocks are running'
        stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null
        stubFinder.findStubUrl('loanIssuance') != null
        stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance')
        stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance')
        stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs')
        stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null
        and:
        stubFinder.findAllRunningStubs().isPresent('loanIssuance')
        stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer')
        stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer')
        and: 'Stubs were registered'
        "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
        "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
        and: 'Fraud Detection is an HTTPS endpoint'
        stubFinder.findStubUrl('fraudDetectionServer').toString().startsWith("https")
    }

    def 'should throw an exception when stub is not found'() {
        when:
        stubFinder.findStubUrl('nonExistingService')
        then:
        thrown(StubNotFoundException)
        when:
        stubFinder.findStubUrl('nonExistingGroupId', 'nonExistingArtifactId')
        then:
        thrown(StubNotFoundException)
    }

    def 'should register started servers as environment variables'() {
        expect:
        environment.getProperty("stubrunner.runningstubs.loanIssuance.port") != null
        stubFinder.findAllRunningStubs().getPort("loanIssuance") == (environment.getProperty("stubrunner.runningstubs.loanIssuance.port") as Integer)
        and:
        environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") != null
        stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") == (environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") as Integer)
        and:
        environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") != null
        stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") == (environment.getProperty("stubrunner.runningstubs.org.springframework.cloud.contract.verifier.stubs.fraudDetectionServer.port") as Integer)
    }

    def 'should be able to interpolate a running stub in the passed test property'() {
        given:
        int fraudPort = stubFinder.findAllRunningStubs().getPort("fraudDetectionServer")
        expect:
        fraudPort > 0
        environment.getProperty("foo", Integer) == fraudPort
        environment.getProperty("fooWithGroup", Integer) == fraudPort
        foo == fraudPort
    }

    @Issue("#573")
    def 'should be able to retrieve the port of a running stub via an annotation'() {
        given:
        int fraudPort = stubFinder.findAllRunningStubs().getPort("fraudDetectionServer")
        expect:
        fraudPort > 0
        fraudDetectionServerPort == fraudPort
        fraudDetectionServerPortWithGroupId == fraudPort
    }

    def 'should dump all mappings to a file'() {
        when:
        def url = stubFinder.findStubUrl("fraudDetectionServer")
        then:
        new File("target/outputmappings/", "fraudDetectionServer_${url.port}").exists()
    }

    @Configuration
    @EnableAutoConfiguration
    static class Config {}

    @CompileStatic
    static class HttpsForFraudDetection extends WireMockHttpServerStubConfigurer {

        private static final Log log = LogFactory.getLog(HttpsForFraudDetection)

        @Override
        WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) {
            if (httpServerStubConfiguration.stubConfiguration.artifactId == "fraudDetectionServer") {
                int httpsPort = SocketUtils.findAvailableTcpPort()
                log.info("Will set HTTPs port [" + httpsPort + "] for fraud detection server")
                return httpStubConfiguration
                        .httpsPort(httpsPort)
            }
            return httpStubConfiguration
        }
    }
}

这样做取决于以下配置文件:spring-doc.cn

stubrunner:
  repositoryRoot: classpath:m2repo/repository/
  ids:
    - org.springframework.cloud.contract.verifier.stubs:loanIssuance
    - org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer
    - org.springframework.cloud.contract.verifier.stubs:bootService
  stubs-mode: remote

除了使用属性之外,您还可以使用 . 以下示例通过在 annotation 上设置值来获得相同的结果:@AutoConfigureStubRunnerspring-doc.cn

@AutoConfigureStubRunner(
        ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance",
                "org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer",
                "org.springframework.cloud.contract.verifier.stubs:bootService"],
        stubsMode = StubRunnerProperties.StubsMode.REMOTE,
        repositoryRoot = "classpath:m2repo/repository/")

Stub Runner Spring 按以下方式注册环境变量 对于每个已注册的 WireMock 服务器。以下示例显示了 和 的存根运行程序 ID:com.example:thing1com.example:thing2spring-doc.cn

您可以在代码中引用这些值。spring-doc.cn

您还可以使用 Comments 注入正在运行的存根的端口。 注释的值可以是 ,也可以只有 。 以下示例作品显示了 和 的 Stub Runner ID。@StubRunnerPortgroupid:artifactidartifactidcom.example:thing1com.example:thing2spring-doc.cn

@StubRunnerPort("thing1")
int thing1Port;
@StubRunnerPort("com.example:thing2")
int thing2Port;

5.5. Stub Runner Spring Cloud

Stub Runner 可以与 Spring Cloud 集成。spring-doc.cn

有关实际示例,请参阅:spring-doc.cn

5.5.1. Stubbing 服务发现

最重要的特征是它存根:Stub Runner Spring Cloudspring-doc.cn

这意味着,无论您使用的是 Zookeeper、Consul、Eureka 还是任何东西 否则,您在测试中不需要它。我们正在启动您的 依赖项,并且我们会告诉您的应用程序,无论何时使用 ,都要加载 balanced 或直接调用这些存根服务器 而不是调用真正的 Service Discovery 工具。FeignRestTemplateDiscoveryClientspring-doc.cn

例如,以下测试通过:spring-doc.cn

def 'should make service discovery work'() {
    expect: 'WireMocks are running'
    "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
    "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
    and: 'Stubs can be reached via load service discovery'
    restTemplate.getForObject('http://loanIssuance/name', String) == 'loanIssuance'
    restTemplate.getForObject('http://someNameThatShouldMapFraudDetectionServer/name', String) == 'fraudDetectionServer'
}

请注意,前面的示例需要以下配置文件:spring-doc.cn

stubrunner:
  idsToServiceIds:
    ivyNotation: someValueInsideYourCode
    fraudDetectionServer: someNameThatShouldMapFraudDetectionServer
测试配置文件和服务发现

在集成测试中,您通常不想调用任何一个发现服务(例如 Eureka) 或 Config Server。因此,您需要创建一个额外的测试配置,并在其中禁用 这些功能。spring-doc.cn

由于 spring-cloud-commons 的某些限制, 为此,您必须禁用这些属性 在如下例(对于 Eureka)的 static 块中:spring-doc.cn

    //Hack to work around https://github.com/spring-cloud/spring-cloud-commons/issues/156
    static {
        System.setProperty("eureka.client.enabled", "false");
        System.setProperty("spring.cloud.config.failFast", "false");
    }

5.5.2. 其他配置

您可以使用 map 将存根的 与应用程序的名称进行匹配。artifactIdstubrunner.idsToServiceIds:spring-doc.cn

默认情况下,所有服务发现都是存根的。这意味着,无论您是否拥有 一个 existing ,其结果将被忽略。但是,如果要重用它,则可以设置为 ,然后现有结果为 与存根的合并。DiscoveryClientstubrunner.cloud.delegate.enabledtrueDiscoveryClient

Stub Runner 使用的默认 Maven 配置可以调整 通过设置以下系统属性或设置相应的环境变量:spring-doc.cn

  • maven.repo.local:自定义 maven 本地存储库位置的路径spring-doc.cn

  • org.apache.maven.user-settings:自定义 maven 用户设置位置的路径spring-doc.cn

  • org.apache.maven.global-settings:maven 全局设置位置的路径spring-doc.cn

5.6. 使用 Stub Runner 引导应用程序

Spring Cloud Contract Stub Runner Boot 是一个 Spring Boot 应用程序,它将 REST 端点公开给 触发消息标签并访问 WireMock 服务器。spring-doc.cn

5.6.1. Stub Runner 引导安全性

Stub Runner Boot 应用程序在设计上并不安全 - 保护它需要为所有应用程序添加安全性 stubs 的 S 文件,即使他们实际上并不需要它。由于这是一个测试实用程序,因此该服务器不打算在生产环境中使用。spring-doc.cn

预计只有受信任的客户端才能访问 Stub Runner Boot 服务器。你不应该 在不受信任的位置以 Fat Jar 或 Docker Image 的形式运行此应用程序。

5.6.2. Stub Runner 服务器

要使用 Stub Runner Server,请添加以下依赖项:spring-doc.cn

compile "org.springframework.cloud:spring-cloud-starter-stub-runner"

然后用 来注释一个类,构建一个胖 jar,它就可以工作了。@EnableStubRunnerServerspring-doc.cn

有关属性,请参阅 Stub Runner Spring 部分。spring-doc.cn

5.6.3. Stub Runner 服务器 Fat Jar

您可以从 Maven 下载独立的 JAR(例如,对于版本 2.0.1.RELEASE) 通过运行以下命令:spring-doc.cn

$ wget -O stub-runner.jar 'https://search.maven.org/remotecontent?filepath=org/springframework/cloud/spring-cloud-contract-stub-runner-boot/2.0.1.RELEASE/spring-cloud-contract-stub-runner-boot-2.0.1.RELEASE.jar'
$ java -jar stub-runner.jar --stubrunner.ids=... --stubrunner.repositoryRoot=...

5.6.4. Spring Cloud CLI

Spring Cloud CLI 项目的版本开始,您可以通过运行 来启动 Stub Runner Boot。1.4.0.RELEASEspring cloud stubrunnerspring-doc.cn

要传递配置,可以在当前工作目录下创建一个文件 在名为 的子目录中,或在 中。该文件可能类似于以下内容 运行本地安装的存根的示例:stubrunner.ymlconfig~/.spring-cloudspring-doc.cn

示例 2.stubrunner.yml
stubrunner:
  stubsMode: LOCAL
  ids:
    - com.example:beer-api-producer:+:9876

然后,您可以从终端窗口调用以启动 Stub Runner 服务器。可在 port 获得。spring cloud stubrunner8750spring-doc.cn

5.6.5. 端点

Stub Runner Boot 提供两个端点:spring-doc.cn

HTTP 协议

对于 HTTP,Stub Runner Boot 使以下终端节点可用:spring-doc.cn

  • GET :返回表示法中所有正在运行的存根的列表/stubsivy:integerspring-doc.cn

  • GET :返回给定表示法的端口(当调用端点也可以是唯一时)/stubs/{ivy}ivyivyartifactIdspring-doc.cn

消息

对于消息收发,Stub Runner Boot 使以下终端节点可用:spring-doc.cn

  • GET :返回表示法中所有运行标签的列表/triggersivy : [ label1, label2 …​]spring-doc.cn

  • POST :使用/triggers/{label}labelspring-doc.cn

  • POST :为给定表示法运行带有 a 的触发器 (调用终端节点时,也可以只调用)/triggers/{ivy}/{label}labelivyivyartifactIdspring-doc.cn

5.6.6. 示例

以下示例显示了 Stub Runner Boot 的典型用法:spring-doc.cn

@SpringBootTest(classes = StubRunnerBoot, properties = "spring.cloud.zookeeper.enabled=false")
@ActiveProfiles("test")
class StubRunnerBootSpec extends Specification {

    @Autowired
    StubRunning stubRunning

    def setup() {
        RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning),
                new TriggerController(stubRunning))
    }

    def 'should return a list of running stub servers in "full ivy:port" notation'() {
        when:
            String response = RestAssuredMockMvc.get('/stubs').body.asString()
        then:
            def root = new JsonSlurper().parseText(response)
            root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs' instanceof Integer
    }

    def 'should return a port on which a [#stubId] stub is running'() {
        when:
            def response = RestAssuredMockMvc.get("/stubs/${stubId}")
        then:
            response.statusCode == 200
            Integer.valueOf(response.body.asString()) > 0
        where:
            stubId << ['org.springframework.cloud.contract.verifier.stubs:bootService:+:stubs',
                       'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs',
                       'org.springframework.cloud.contract.verifier.stubs:bootService:+',
                       'org.springframework.cloud.contract.verifier.stubs:bootService',
                       'bootService']
    }

    def 'should return 404 when missing stub was called'() {
        when:
            def response = RestAssuredMockMvc.get("/stubs/a:b:c:d")
        then:
            response.statusCode == 404
    }

    def 'should return a list of messaging labels that can be triggered when version and classifier are passed'() {
        when:
            String response = RestAssuredMockMvc.get('/triggers').body.asString()
        then:
            def root = new JsonSlurper().parseText(response)
            root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs'?.containsAll(["delete_book", "return_book_1", "return_book_2"])
    }

    def 'should trigger a messaging label'() {
        given:
            StubRunning stubRunning = Mock()
            RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
        when:
            def response = RestAssuredMockMvc.post("/triggers/delete_book")
        then:
            response.statusCode == 200
        and:
            1 * stubRunning.trigger('delete_book')
    }

    def 'should trigger a messaging label for a stub with [#stubId] ivy notation'() {
        given:
            StubRunning stubRunning = Mock()
            RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
        when:
            def response = RestAssuredMockMvc.post("/triggers/$stubId/delete_book")
        then:
            response.statusCode == 200
        and:
            1 * stubRunning.trigger(stubId, 'delete_book')
        where:
            stubId << ['org.springframework.cloud.contract.verifier.stubs:bootService:stubs', 'org.springframework.cloud.contract.verifier.stubs:bootService', 'bootService']
    }

    def 'should throw exception when trigger is missing'() {
        when:
            RestAssuredMockMvc.post("/triggers/missing_label")
        then:
            Exception e = thrown(Exception)
            e.message.contains("Exception occurred while trying to return [missing_label] label.")
            e.message.contains("Available labels are")
            e.message.contains("org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs=[]")
            e.message.contains("org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs=")
    }

}

5.6.7. 带有服务发现的 Stub Runner 引导

使用 Stub Runner Boot 的一种方法是将其用作 “冒烟测试” 的存根源。那是什么意思? 假设您不想为了 Order Order 将 50 个微服务部署到测试环境中 以查看您的应用程序是否正常工作。您已经在构建过程中运行了一套测试。 但您还希望确保应用程序的打包有效。您可以 将应用程序部署到环境,启动它,然后对其运行几个测试以查看 它有效。我们可以将这些测试称为 “冒烟测试”,因为它们的目的是只检查少数几个 的测试场景。spring-doc.cn

这种方法的问题在于,如果您使用微服务,您很可能还会 使用服务发现工具。Stub Runner Boot 允许您通过启动 必需的存根,并在服务发现工具中注册它们。请考虑以下示例 使用 Eureka 进行这样的设置(假设 Eureka 已经在运行):spring-doc.cn

@SpringBootApplication
@EnableStubRunnerServer
@EnableEurekaClient
@AutoConfigureStubRunner
public class StubRunnerBootEurekaExample {

    public static void main(String[] args) {
        SpringApplication.run(StubRunnerBootEurekaExample.class, args);
    }

}

我们想要启动一个 Stub Runner Boot 服务器 (),启用 Eureka 客户端 (), 并打开 Stub Runner 功能 ()。@EnableStubRunnerServer@EnableEurekaClient@AutoConfigureStubRunnerspring-doc.cn

现在假设我们要启动此应用程序,以便自动注册存根。 为此,我们可以使用 运行应用程序,其中包含以下属性列表:java -jar ${SYSTEM_PROPS} stub-runner-boot-eureka-example.jar${SYSTEM_PROPS}spring-doc.cn

* -Dstubrunner.repositoryRoot=https://repo.spring.io/snapshot (1)
* -Dstubrunner.cloud.stubbed.discovery.enabled=false (2)
* -Dstubrunner.ids=org.springframework.cloud.contract.verifier.stubs:loanIssuance,org.
* springframework.cloud.contract.verifier.stubs:fraudDetectionServer,org.springframework.
* cloud.contract.verifier.stubs:bootService (3)
* -Dstubrunner.idsToServiceIds.fraudDetectionServer=
* someNameThatShouldMapFraudDetectionServer (4)
*
* (1) - we tell Stub Runner where all the stubs reside (2) - we don't want the default
* behaviour where the discovery service is stubbed. That's why the stub registration will
* be picked (3) - we provide a list of stubs to download (4) - we provide a list of

这样,您部署的应用程序可以通过 service 向已启动的 WireMock 服务器发送请求 发现。点 1 到 3 很可能默认在 中设置,因为它们不是 可能会改变。这样,您可以只提供启动时要下载的存根列表 Stub Runner 靴子。application.ymlspring-doc.cn

5.7. 消费者驱动的合约:每个消费者的存根

在某些情况下,同一终端节点的两个使用者希望有两个不同的响应。spring-doc.cn

此方法还可以让您立即知道哪个使用者使用您 API 的哪个部分。 您可以删除 API 生成的响应的一部分,并查看哪些自动生成的测试 失败。如果没有失败,您可以安全地删除响应的该部分,因为没有人使用它。

请考虑以下为生产者定义的 Contract 示例,该 Contract 名为 , 它有两个使用者 ( 和 ):producerfoo-consumerbar-consumerspring-doc.cn

消费者foo-service
request {
   url '/foo'
   method GET()
}
response {
    status OK()
    body(
       foo: "foo"
    }
}
消费者bar-service
request {
   url '/bar'
   method GET()
}
response {
    status OK()
    body(
       bar: "bar"
    }
}

您不能为同一请求生成两个不同的响应。这就是为什么你可以正确地打包 合同,然后从该功能中获利。stubsPerConsumerspring-doc.cn

在创建者端,使用者可以拥有一个文件夹,其中包含仅与他们相关的协定。 通过将标志设置为 ,我们不再注册所有存根,而只注册那些 对应于使用者应用程序的名称。换句话说,我们扫描每个存根的路径,然后, 如果它包含路径中具有使用者名称的子文件夹,则只有这样才会注册它。stubrunner.stubs-per-consumertruespring-doc.cn

在生产者方面,合同将如下所示foospring-doc.cn

.
└── contracts
    ├── bar-consumer
    │   ├── bookReturnedForBar.groovy
    │   └── shouldCallBar.groovy
    └── foo-consumer
        ├── bookReturnedForFoo.groovy
        └── shouldCallFoo.groovy

使用者可以将 或 设置为 或者,您可以按如下方式设置测试:bar-consumerspring.application.namestubrunner.consumer-namebar-consumerspring-doc.cn

@SpringBootTest(classes = Config, properties = ["spring.application.name=bar-consumer"])
@AutoConfigureStubRunner(ids = "org.springframework.cloud.contract.verifier.stubs:producerWithMultipleConsumers",
        repositoryRoot = "classpath:m2repo/repository/",
        stubsMode = StubRunnerProperties.StubsMode.REMOTE,
        stubsPerConsumer = true)
@ActiveProfiles("streamconsumer")
class StubRunnerStubsPerConsumerSpec extends Specification {
...
}

然后,仅允许引用在其名称中包含 path (即文件夹中的存根) 下注册的存根。bar-consumersrc/test/resources/contracts/bar-consumer/some/contracts/…​spring-doc.cn

您还可以显式设置使用者名称,如下所示:spring-doc.cn

@SpringBootTest(classes = Config)
@AutoConfigureStubRunner(ids = "org.springframework.cloud.contract.verifier.stubs:producerWithMultipleConsumers",
        repositoryRoot = "classpath:m2repo/repository/",
        consumerName = "foo-consumer",
        stubsMode = StubRunnerProperties.StubsMode.REMOTE,
        stubsPerConsumer = true)
@ActiveProfiles("streamconsumer")
class StubRunnerStubsPerConsumerWithConsumerNameSpec extends Specification {
...
}

然后,只允许在名称中包含 the 的路径下注册的存根 (即文件夹中的存根) 被引用。foo-consumersrc/test/resources/contracts/foo-consumer/some/contracts/…​spring-doc.cn

有关此更改背后的原因的更多信息, 参见 问题 224spring-doc.cn

5.8. 从某个位置获取存根或 Contract 定义

而不是从 您可以指向 Artifactory、Nexus 或 Git 驱动器或 Classpath 上的位置。这样做在多模块项目中特别有用,因为一个模块需要 重用来自另一个模块的存根或 Contract 而不 需要在本地 maven 中实际安装这些 存储库将这些更改提交到 Git。spring-doc.cn

为了实现此目的,您可以在设置存储库根参数时使用协议 在 Stub Runner 或 Spring Cloud Contract 插件中。stubs://spring-doc.cn

在此示例中,项目已成功 built 并在该文件夹下生成存根。作为使用者,可以使用协议设置 Stub Runner 以从该位置选取存根。producertarget/stubsstubs://spring-doc.cn

注解
@AutoConfigureStubRunner(
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
        repositoryRoot = "stubs://file://location/to/the/producer/target/stubs/",
        ids = "com.example:some-producer")
JUnit 4 规则
@Rule
    public StubRunnerRule rule = new StubRunnerRule()
            .downloadStub("com.example:some-producer")
            .repoRoot("stubs://file://location/to/the/producer/target/stubs/")
            .stubsMode(StubRunnerProperties.StubsMode.REMOTE);
JUnit 5 扩展
@RegisterExtension
    public StubRunnerExtension stubRunnerExtension = new StubRunnerExtension()
            .downloadStub("com.example:some-producer")
            .repoRoot("stubs://file://location/to/the/producer/target/stubs/")
            .stubsMode(StubRunnerProperties.StubsMode.REMOTE);

Contract 和 stub 可以存储在一个位置,每个 producer 都有自己的专用文件夹,用于 Contract 和 stub 映射。在该文件夹下,每个使用者都可以有自己的设置。要使 Stub Runner 从提供的 ID 中查找专用文件夹,您可以传递 property 或 system 属性。 下面的清单显示了 Contract 和 stub 的排列:stubs.find-producer=truestubrunner.stubs.find-producer=truespring-doc.cn

└── com.example (1)
    ├── some-artifact-id (2)
    │   └── 0.0.1
    │       ├── contracts (3)
    │       │   └── shouldReturnStuffForArtifactId.groovy
    │       └── mappings (4)
    │           └── shouldReturnStuffForArtifactId.json
    └── some-other-artifact-id (5)
        ├── contracts
        │   └── shouldReturnStuffForOtherArtifactId.groovy
        └── mappings
            └── shouldReturnStuffForOtherArtifactId.json
1 消费者的 Group ID
2 具有工件 ID [some-artifact-id] 的使用者
3 具有工件 ID [some-artifact-id] 的使用者的合同
4 具有工件 ID [some-artifact-id] 的使用者的映射
5 具有工件 ID [some-other-artifact-id] 的使用者
注解
@AutoConfigureStubRunner(
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
        repositoryRoot = "stubs://file://location/to/the/contracts/directory",
        ids = "com.example:some-producer",
        properties="stubs.find-producer=true")
JUnit 4 规则
    static Map<String, String> contractProperties() {
        Map<String, String> map = new HashMap<>();
        map.put("stubs.find-producer", "true");
        return map;
    }

@Rule
    public StubRunnerRule rule = new StubRunnerRule()
            .downloadStub("com.example:some-producer")
            .repoRoot("stubs://file://location/to/the/contracts/directory")
            .stubsMode(StubRunnerProperties.StubsMode.REMOTE)
            .properties(contractProperties());
JUnit 5 扩展
    static Map<String, String> contractProperties() {
        Map<String, String> map = new HashMap<>();
        map.put("stubs.find-producer", "true");
        return map;
    }

@RegisterExtension
    public StubRunnerExtension stubRunnerExtension = new StubRunnerExtension()
            .downloadStub("com.example:some-producer")
            .repoRoot("stubs://file://location/to/the/contracts/directory")
            .stubsMode(StubRunnerProperties.StubsMode.REMOTE)
            .properties(contractProperties());

5.9. 在运行时生成存根

作为使用者,您可能不想等待创建者完成其实现,然后发布其存根。此问题的解决方案是在运行时生成存根。spring-doc.cn

作为创建者,在定义合同时,您需要使生成的测试通过,以便发布存根。在某些情况下,您希望取消阻止使用者,以便他们可以在测试实际通过之前获取存根。在这种情况下,您应该将此类合同设置为 in-progress。您可以在 Contracts in Progress 部分阅读有关此内容的更多信息。这样,不会生成测试,但会生成存根。spring-doc.cn

作为使用者,您可以切换开关以在运行时生成存根。Stub Runner 会忽略所有现有的存根映射,并为所有 Contract 定义生成新的存根映射。另一个选项是传递 system 属性。以下示例显示了此类设置:stubrunner.generate-stubsspring-doc.cn

注解
@AutoConfigureStubRunner(
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
        repositoryRoot = "stubs://file://location/to/the/contracts",
        ids = "com.example:some-producer",
        generateStubs = true)
JUnit 4 规则
@Rule
    public StubRunnerRule rule = new StubRunnerRule()
            .downloadStub("com.example:some-producer")
            .repoRoot("stubs://file://location/to/the/contracts")
            .stubsMode(StubRunnerProperties.StubsMode.REMOTE)
            .withGenerateStubs(true);
JUnit 5 扩展
@RegisterExtension
    public StubRunnerExtension stubRunnerExtension = new StubRunnerExtension()
            .downloadStub("com.example:some-producer")
            .repoRoot("stubs://file://location/to/the/contracts")
            .stubsMode(StubRunnerProperties.StubsMode.REMOTE)
            .withGenerateStubs(true);

5.10. 无桩失败

默认情况下,如果未找到存根,则 Stub Runner 将失败。要更改该行为,请在注释中将属性设置为,或者对 JUnit 规则或扩展调用该方法。以下示例显示了如何执行此操作:failOnNoStubsfalsewithFailOnNoStubs(false)spring-doc.cn

注解
@AutoConfigureStubRunner(
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
        repositoryRoot = "stubs://file://location/to/the/contracts",
        ids = "com.example:some-producer",
        failOnNoStubs = false)
JUnit 4 规则
@Rule
    public StubRunnerRule rule = new StubRunnerRule()
            .downloadStub("com.example:some-producer")
            .repoRoot("stubs://file://location/to/the/contracts")
            .stubsMode(StubRunnerProperties.StubsMode.REMOTE)
            .withFailOnNoStubs(false);
JUnit 5 扩展
@RegisterExtension
    public StubRunnerExtension stubRunnerExtension = new StubRunnerExtension()
            .downloadStub("com.example:some-producer")
            .repoRoot("stubs://file://location/to/the/contracts")
            .stubsMode(StubRunnerProperties.StubsMode.REMOTE)
            .withFailOnNoStubs(false);

5.11. 通用属性

本节简要介绍常见的属性,包括:spring-doc.cn

5.11.1. JUnit 和 Spring 的通用属性

您可以使用系统属性或 Spring 配置来设置重复属性 性能。下表显示了它们的名称及其默认值:spring-doc.cn

属性名称 默认值 描述

stubrunner.minPortspring-doc.cn

10000spring-doc.cn

带有存根的已启动 WireMock 的端口最小值。spring-doc.cn

stubrunner.maxPortspring-doc.cn

15000spring-doc.cn

带有存根的已启动 WireMock 的端口的最大值。spring-doc.cn

stubrunner.repositoryRootspring-doc.cn

Maven 存储库 URL。如果为空,则调用本地 Maven 存储库。spring-doc.cn

stubrunner.classifierspring-doc.cn

stubsspring-doc.cn

存根工件的默认分类器。spring-doc.cn

stubrunner.stubsModespring-doc.cn

CLASSPATHspring-doc.cn

你想要获取和注册存根的方式。spring-doc.cn

stubrunner.idsspring-doc.cn

要下载的 Ivy 表示法存根数组。spring-doc.cn

stubrunner.usernamespring-doc.cn

可选的 username 来访问存储 JAR 的工具 存根。spring-doc.cn

stubrunner.passwordspring-doc.cn

用于访问存储 JAR 的工具的可选密码 存根。spring-doc.cn

stubrunner.stubsPerConsumerspring-doc.cn

falsespring-doc.cn

如果要对 每个使用者,而不是为每个使用者注册所有存根。truespring-doc.cn

stubrunner.consumerNamespring-doc.cn

如果您想为每个使用者使用一个存根,并且希望 覆盖使用者名称,更改此值。spring-doc.cn

5.11.2. Stub Runner 存根 ID

您可以在 system 属性中将存根设置为 download。他们 使用以下模式:stubrunner.idsspring-doc.cn

groupId:artifactId:version:classifier:port

请注意,、 和 是可选的。versionclassifierportspring-doc.cn

  • 如果您不提供 ,则会随机选择一个。portspring-doc.cn

  • 如果未提供 ,则使用默认值。(请注意,您可以 以这种方式传递一个空分类器:)。classifiergroupId:artifactId:version:spring-doc.cn

  • 如果未提供 ,则传递 ,最新的是 下载。version+spring-doc.cn

port表示 WireMock 服务器的端口。spring-doc.cn

从版本 1.0.4 开始,您可以提供一系列版本,您可以 希望 Stub Runner 考虑在内。您可以阅读有关 此处的 Aether 版本控制范围。

6. Spring Cloud 合约 WireMock

Spring Cloud Contract WireMock 模块允许您在 Spring Boot 应用程序。有关更多详细信息,请查看示例spring-doc.cn

如果您有一个使用 Tomcat 作为嵌入式服务器的 Spring Boot 应用程序(即 默认值为 ),你可以添加到 Classpath 中,并添加以在测试中使用 Wiremock。Wiremock 作为存根服务器运行,而您 可以通过使用 Java API 或使用静态 JSON 声明作为 你的测试。下面的代码显示了一个示例:spring-boot-starter-webspring-cloud-starter-contract-stub-runner@AutoConfigureWireMockspring-doc.cn

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
public class WiremockForDocsTests {

    // A service that calls out over HTTP
    @Autowired
    private Service service;

    @BeforeEach
    public void setup() {
        this.service.setBase("http://localhost:"
                + this.environment.getProperty("wiremock.server.port"));
    }

    // Using the WireMock APIs in the normal way:
    @Test
    public void contextLoads() throws Exception {
        // Stubbing WireMock
        stubFor(get(urlEqualTo("/resource")).willReturn(aResponse()
                .withHeader("Content-Type", "text/plain").withBody("Hello World!")));
        // We're asserting if WireMock responded properly
        assertThat(this.service.go()).isEqualTo("Hello World!");
    }

}

要在其他端口上启动存根服务器,请使用 (例如)、.对于随机端口,请使用值 。存根 服务器端口可以在测试应用程序上下文中使用属性 .Using 将 测试应用程序上下文,缓存在方法和类之间 具有相同的上下文。Spring 集成测试也是如此。此外,您还可以 将 type 的 bean 注入到测试中。 在每个测试类之后,将重置已注册的 WireMock 服务器。 但是,如果需要在每个测试方法之后重置它,请将该属性设置为 。@AutoConfigureWireMock(port=9999)0wiremock.server.port@AutoConfigureWireMockWiremockConfigurationWireMockServerwiremock.reset-mappings-after-each-testtruespring-doc.cn

6.1. 自动注册存根

如果使用 ,它会从文件中注册 WireMock JSON 存根 system 或 classpath (默认情况下,从 )。您可以 使用注释中的属性自定义位置,该属性可以是 Ant 样式的资源模式或目录。对于目录,是 附加。下面的代码显示了一个示例:@AutoConfigureWireMockfile:src/test/resources/mappingsstubs*/.jsonspring-doc.cn

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureWireMock(stubs="classpath:/stubs")
public class WiremockImportApplicationTests {

    @Autowired
    private Service service;

    @Test
    public void contextLoads() throws Exception {
        assertThat(this.service.go()).isEqualTo("Hello World!");
    }

}
实际上,WireMock 总是从 以及属性中的自定义位置。要更改此行为,您可以 此外,指定 File Root,如本文档的下一节所述。src/test/resources/mappingsstubs
此外,该位置的映射不被视为 Wiremock 的“默认映射”的一部分,并调用 to 不会导致映射 在所包含的位置。但是,它会在每个测试类之后重置映射(包括从 stubs 位置添加映射),并且(可选)会重置 在每个测试方法之后(由属性保护)。stubscom.github.tomakehurst.wiremock.client.WireMock.resetToDefaultMappingsstubsorg.springframework.cloud.contract.wiremock.WireMockTestExecutionListenerwiremock.reset-mappings-after-each-test

如果您使用 Spring Cloud Contract 的默认存根 jar,则您的 存根存储在文件夹中。 如果要从该位置注册所有嵌入式 JAR 中的所有存根,可以使用 以下语法:/META-INF/group-id/artifact-id/versions/mappings/spring-doc.cn

@AutoConfigureWireMock(port = 0, stubs = "classpath*:/META-INF...

6.2. 使用文件指定存根体

WireMock 可以从 Classpath 或文件系统上的文件中读取响应体。在 的情况下,您可以在 JSON DSL 中看到响应具有 a 而不是 (文字) 。文件是相对于根目录解析的(默认情况下为 )。要自定义此位置,可以将 annotation 中的属性设置为父项的位置 directory (换句话说,是一个子目录)。您可以使用 Spring 资源 表示法 (notation) 来引用或位置。通用 URL 不是 支持。可以给出值列表 — 在这种情况下,WireMock 解析第一个文件 当它需要查找响应正文时,它就存在了。bodyFileNamebodysrc/test/resources/__filesfiles@AutoConfigureWireMock__filesfile:…​classpath:…​spring-doc.cn

配置根时,它还会影响 自动加载存根,因为它们来自根位置 在名为 .filesmappings
的值没有 对从属性显式加载的存根的影响。filesstubs

6.3. 替代方案:使用 JUnit 规则

对于更传统的 WireMock 体验,您可以使用 JUnit 来启动和停止 服务器。为此,请使用 convenience 类获取实例,如下例所示:@RulesWireMockSpringOptionsspring-doc.cn

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

    // Start WireMock on some dynamic port
    // for some reason `dynamicPort()` is not working properly
    public static WireMockServer wiremock = new WireMockServer(WireMockSpring.options().dynamicPort());

    @BeforeAll
    static void setupClass() {
        wiremock.start();
    }

    @AfterEach
    void after() {
        wiremock.resetAll();
    }

    @AfterAll
    static void clean() {
        wiremock.shutdown();
    }

    // A service that calls out over HTTP to wiremock's port
    @Autowired
    private Service service;

    @BeforeEach
    public void setup() {
        this.service.setBase("http://localhost:" + wiremock.port());
    }

    // Using the WireMock APIs in the normal way:
    @Test
    public void contextLoads() throws Exception {
        // Stubbing WireMock
        wiremock.stubFor(get(urlEqualTo("/resource")).willReturn(aResponse()
                .withHeader("Content-Type", "text/plain").withBody("Hello World!")));
        // We're asserting if WireMock responded properly
        assertThat(this.service.go()).isEqualTo("Hello World!");
    }

}

这意味着服务器在完成此类中的所有方法后关闭 已运行。@ClassRulespring-doc.cn

6.4. Rest 模板的宽松 SSL 验证

WireMock 允许您使用 URL 协议对 “安全” 服务器进行存根。如果您的 应用程序想要在集成测试中联系该存根服务器,它发现 SSL 证书无效(自行安装的证书的常见问题)。 最好的选择通常是重新配置客户端以使用 .如果那不是 选项,你可以要求 Spring 配置一个忽略 SSL 验证错误的 HTTP 客户端 (当然,仅对测试执行此操作)。httpshttpspring-doc.cn

要使这项工作最小化,您需要在应用程序中使用 Spring Boot,如下例所示:RestTemplateBuilderspring-doc.cn

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
    return builder.build();
}

你需要,因为构建器是通过回调传递给 初始化它,以便可以在此时在客户端中设置 SSL 验证。这 如果您使用 Annotation 或 Stub Runner,则会自动在测试中发生。如果使用 JUnit 方法,则还需要添加注释,如下例所示:RestTemplateBuilder@AutoConfigureWireMock@Rule@AutoConfigureHttpClientspring-doc.cn

@RunWith(SpringRunner.class)
@SpringBootTest("app.baseUrl=https://localhost:6443")
@AutoConfigureHttpClient
public class WiremockHttpsServerApplicationTests {

    @ClassRule
    public static WireMockClassRule wiremock = new WireMockClassRule(
            WireMockSpring.options().httpsPort(6443));
...
}

如果使用 ,则 Apache HTTP 客户端位于 classpath 的 API 路径,它由 选择并配置为忽略 SSL 错误。如果使用默认客户端,则不需要 Annotation(但它 没有坏处)。目前不支持其他客户端,但可能会添加 在将来的版本中。spring-boot-starter-testRestTemplateBuilderjava.netspring-doc.cn

要禁用自定义 ,请将属性设置为 。RestTemplateBuilderwiremock.rest-template-ssl-enabledfalsespring-doc.cn

6.5. WireMock 和 Spring MVC 模拟

Spring Cloud Contract 提供了一个方便的类,可以将 JSON WireMock 存根加载到 一个弹簧 .下面的代码显示了一个示例:MockRestServiceServerspring-doc.cn

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class WiremockForDocsMockServerApplicationTests {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private Service service;

    @Test
    public void contextLoads() throws Exception {
        // will read stubs classpath
        MockRestServiceServer server = WireMockRestServiceServer.with(this.restTemplate)
                .baseUrl("https://example.org").stubs("classpath:/stubs/resource.json")
                .build();
        // We're asserting if WireMock responded properly
        assertThat(this.service.go()).isEqualTo("Hello World");
        server.verify();
    }

}

该值附加到所有 mock 调用的前面,并且该方法采用存根 path 资源模式作为参数。在前面的示例中,定义的存根 at 被加载到模拟服务器中。如果要求 访问 ,它会获取在该 URL 中声明的响应。更多 可以指定一个存根模式,并且每个存根模式可以是一个目录(对于递归的 list of all )、固定文件名(如前面的示例所示)或 Ant 样式 模式。JSON 格式是普通的 WireMock 格式,您可以在 WireMock 网站上阅读有关该格式的信息。baseUrlstubs()/stubs/resource.jsonRestTemplateexample.org/.jsonspring-doc.cn

目前,Spring Cloud Contract Verifier 支持 Tomcat、Jetty 和 Undertow 作为 Spring Boot 嵌入式服务器,而 Wiremock 本身对特定的 Jetty 版本(当前为 9.2)。要使用本机 Jetty,您需要将本机 Wiremock 依赖项并排除 Spring Boot 容器(如果有)。spring-doc.cn

7. 构建工具集成

您可以通过多种方式运行测试生成和存根调用。最常见的是 如下:spring-doc.cn

8. 下一步要读什么

如果您想了解有关本节中讨论的任何类的更多信息,可以直接浏览源代码。如果您有具体问题,请参阅操作方法部分。spring-doc.cn

如果您对 Spring Cloud Contract 的核心功能感到满意,则可以继续阅读 关于 Spring Cloud Contract 的高级功能spring-doc.cn