Spring Cloud 合约功能

1. 合约 DSL

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

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

以下示例显示了合同定义:spring-doc.cadn.net.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()
    }
}
YML
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.cadn.net.cn

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

1.1. Groovy 中的合约 DSL

如果您不熟悉 Groovy,请不要担心 - 您也可以在Groovy DSL 文件中使用 Java 语法。spring-doc.cadn.net.cn

如果您决定在 Groovy 中编写合同,如果您还没有使用 Groovy,请不要惊慌 以前。 实际上并不需要该语言的知识,因为合约 DSL 只使用它的一小部分(仅文字、方法调用和闭包)。此外,DSL 是静态的类型化的,以使其程序员无需了解 DSL 本身即可阅读。spring-doc.cadn.net.cn

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

1.2. Java 中的合约 DSL

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

您还可以在src/test/java(例如src/test/java/contracts),这样您就不必修改项目的类路径。在这种情况下,您必须向 Spring Cloud Contract 插件提供契约定义的新位置。spring-doc.cadn.net.cn

专家
<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 脚本文件 (.kts) 开始。就像使用 Java DSL 一样,您可以将合约放在您选择的任何目录中。Maven 和 Gradle 插件将查看src/test/resources/contracts默认情况下,目录。spring-doc.cadn.net.cn

您需要显式传递spring-cloud-contract-spec-kotlin依赖于你的项目插件设置。spring-doc.cadn.net.cn

专家
<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}"
        // remember to add this:
        classpath "org.springframework.cloud:spring-cloud-contract-spec-kotlin:${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 脚本文件中,您必须向ContractDSL类。 通常,您会像这样使用它的合约函数:org.springframework.cloud.contract.spec.ContractDsl.contract { …​ }. 您还可以将导入提供到contract函数 (import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract),然后调用contract { …​ }.

1.4. YML 中的合约 DSL

要查看 YAML 合约的模式,您可以查看 YML 模式页面。spring-doc.cadn.net.cn

1.5. 限制

对验证 JSON 数组大小的支持是实验性的。 如果需要帮助, 要打开它,请将以下系统属性的值设置为true:spring.cloud.contract.verifier.assert.size. 默认情况下,此功能设置为false. 您还可以将assertJsonSize插件配置中的属性。
因为 JSON 结构可以有任何形式,所以不可能解析它正确使用 Groovy DSL 和value(consumer(…​), producer(…​))符号GString. 那 这就是为什么您应该使用 Groovy Map 表示法。

1.6. 常见的顶级元素

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

1.6.1. 描述

您可以添加一个description到你的合同。描述是任意文本。这 以下代码显示了一个示例:spring-doc.cadn.net.cn

槽的
            org.springframework.cloud.contract.spec.Contract.make {
                description('''
given:
    An input
when:
    Sth happens
then:
    Output
''')
            }
YML
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. 名称

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

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

以下示例演示如何向合约添加名称:spring-doc.cadn.net.cn

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

1.6.3. 忽略合约

如果要忽略合约,可以在 插件配置或将ignored合同本身的财产。以下内容 示例显示了如何执行此作:spring-doc.cadn.net.cn

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

1.6.4. 正在进行的合同

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

请谨慎使用此功能,因为它可能会导致误报。您生成存根供消费者使用,而无需实际实现!

如果要设置正在进行的合同,请执行以下作 示例显示了如何执行此作:spring-doc.cadn.net.cn

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

您可以设置failOnInProgressSpring Cloud Contract 插件属性,以确保当源代码中至少保留一个正在进行的合约时,您的构建将中断。spring-doc.cadn.net.cn

1.6.5. 从文件传递值

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

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

进一步假设您的合同如下:spring-doc.cadn.net.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())
        }
    }
}
YML
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.cadn.net.cn

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

当测试或存根生成时,request.jsonresponse.json文件传递给正文 请求或响应。文件名必须是带有位置的文件 相对于合同所在的文件夹。spring-doc.cadn.net.cn

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

以下示例演示如何传递二进制文件的内容:spring-doc.cadn.net.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())
        }
    }
}
YML
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 和消息传递。

2. HTTP 的合约

Spring Cloud Contract 允许您验证使用 REST 或 HTTP 作为 通讯方式。Spring Cloud Contract 验证,对于与 标准request作为合约的一部分,服务器提供位于 与response合同的一部分。随后,合同用于 生成 WireMock 存根,对于与所提供条件匹配的任何请求,提供 合适的回应。spring-doc.cadn.net.cn

2.1. HTTP 顶级元素

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

以下示例演示如何定义 HTTP 请求协定:spring-doc.cadn.net.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
}
YML
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
}
如果你想让你的合同有更高的优先级, 您需要将较低的数字传递给priority标签或方法。例如,一个priority跟 值为5优先级高于priority值为10.

2.2. HTTP 请求

HTTP 协议只需要在请求中指定方法和 URL。这 在合同的请求定义中必须提供相同的信息。spring-doc.cadn.net.cn

以下示例显示了请求的协定:spring-doc.cadn.net.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
    }
}
YML
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)
    }
}

您可以指定绝对值而不是相对值url,但使用urlPath是 推荐的方式,因为这样做会使测试独立于主机。spring-doc.cadn.net.cn

以下示例使用url:spring-doc.cadn.net.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
    }
}
YML
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可能包含查询参数,如以下示例(使用urlPath)显示:spring-doc.cadn.net.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
    }
}
YML
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.cadn.net.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
    }
}
YML
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.cadn.net.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
    }
}
YML
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.cadn.net.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
    }
}
YML
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方法/部分,如以下示例所示:spring-doc.cadn.net.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
    }
}
YML
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.cadn.net.cn

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

  • 通过使用named(…​)方法,用于设置命名参数。命名参数 可以设置一个namecontent.您可以使用具有两个参数的方法调用它 如named("fileName", "fileContent"),或使用映射表示法,例如named(name: "fileName", content: "fileContent").spring-doc.cadn.net.cn

YAML
  • 多部分参数在multipart.params部分。spring-doc.cadn.net.cn

  • 命名参数(fileNamefileContent对于给定的参数名称) 可以在multipart.named部分。该部分包含 这paramName(参数名称),fileName(文件名),fileContent(文件的内容)字段。spring-doc.cadn.net.cn

  • 动态位可以通过matchers.multipart部分。spring-doc.cadn.net.cn

    • 对于参数,请使用params部分,可以接受regexpredefined正则表达式。spring-doc.cadn.net.cn

    • 对于命名参数,请使用named首先你 定义参数名称paramName.然后你可以传递 参数化fileNamefileContentregex或在predefined正则表达式。spring-doc.cadn.net.cn

从前面示例中的协定中,生成的测试和存根如下所示:spring-doc.cadn.net.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.cadn.net.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()
    }
}
YML
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
    }
}

除了状态之外,响应还可以包含标头、cookie 和正文,它们是 指定方式与请求中相同(请参阅 HTTP 请求)。spring-doc.cadn.net.cn

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

2.4. 动态属性

合约可以包含一些动态属性:时间戳、ID 等。您不会 想要强制消费者存根他们的时钟以始终返回相同的时间值 以便它与存根匹配。spring-doc.cadn.net.cn

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

在 2.0.0 之前,这些是通过使用testMatchersstubMatchers. 有关详细信息,请参阅迁移指南

对于 YAML,您只能使用matchers部分。spring-doc.cadn.net.cn

内的条目matchers必须引用有效负载的现有元素。有关详细信息,请检查此问题

2.4.1. Body 内部的动态属性

本节仅对编码 DSL(Groovy、Java 等)有效。查看“匹配器部分”部分中的动态属性,了解类似功能的 YAML 示例。

您可以使用value方法,或者,如果您使用 Groovy 映射表示法,使用 .以下示例演示如何设置动态 属性,并采用 value 方法:$()spring-doc.cadn.net.cn

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

这两种方法同样有效。这stubclient方法是consumer方法。后续部分将仔细研究您可以对这些值执行哪些作。spring-doc.cadn.net.cn

2.4.2. 正则表达式

本节仅对 Groovy DSL 有效。查看 Matchers Sections 部分中的 Dynamic Properties,了解类似功能的 YAML 示例。

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

确保正则表达式与序列的整个区域匹配,就像在内部调用一样Pattern.matches()被称为。例如abc不匹配aabc.abc确实。 还有一些其他已知的限制spring-doc.cadn.net.cn

以下示例演示如何使用正则表达式编写请求:spring-doc.cadn.net.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.cadn.net.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")
        }
    }
}

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

Spring Cloud Contract 附带了一系列预定义的正则表达式,您可以 在合同中使用,如以下示例所示:spring-doc.cadn.net.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 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();
}

在您的合约中,您可以按如下方式使用它(Groovy DSL 的示例):spring-doc.cadn.net.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]'))}]"
        )
    }
}

为了让事情变得更简单,您可以使用一组自动 假设您希望传递正则表达式。 所有这些方法都以any前缀,如下所示:spring-doc.cadn.net.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.cadn.net.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")
        }
    }
}
局限性
由于某些限制Xeger从 正则表达式,如果您依赖自动,请勿在正则表达式中使用 and 符号 代。见第899期$^
不要使用LocalDateinstance 作为值(例如$$(consumer(LocalDate.now()))). 它会导致java.lang.StackOverflowError.用$(consumer(LocalDate.now().toString()))相反。 见第 900 期

2.4.3. 传递可选参数

本节仅对 Groovy DSL 有效。查看 Matchers Sections 部分中的 Dynamic Properties,了解类似功能的 YAML 示例。

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

以下示例演示如何提供可选参数:spring-doc.cadn.net.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")))
        ))
    }
}

通过将身体的一部分包裹起来optional()方法,您创建一个正则表达式,该表达式必须出现 0 次或多次。spring-doc.cadn.net.cn

如果使用 Spock,则将从前面的示例中生成以下测试:spring-doc.cadn.net.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.*

@SuppressWarnings("rawtypes")
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.cadn.net.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 有效。查看 Matchers Sections 部分中的 Dynamic Properties,了解类似功能的 YAML 示例。

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

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

以下代码显示了测试用例的基类部分:spring-doc.cadn.net.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
    }

}
不能同时使用Stringexecute以执行串联。为 示例,调用header('Authorization', 'Bearer ' + execute('authToken()'))导致 结果不当。相反,请调用header('Authorization', execute('authToken()'))和 确保authToken()方法返回您需要的一切。

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

在合同的请求部分,您可以指定body应该取自 一种方法。spring-doc.cadn.net.cn

您必须同时提供消费者和生产者。这execute部分 适用于整个身体,而不是部分身体。

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

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

前面的示例导致调用hashCode()请求正文中的方法。 它应该类似于以下代码:spring-doc.cadn.net.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 在您的响应中。spring-doc.cadn.net.cn

如果您在 Groovy DSL 中编写合约,则可以使用fromRequest()方法,这允许 您引用了 HTTP 请求中的一堆元素。您可以使用以下 选项:spring-doc.cadn.net.cn

如果您使用 YAML 合约定义或 Java 合约定义,则必须将 Handlebars 表示法与自定义 Spring Cloud 合约一起使用 函数来实现这一点。在这种情况下,您可以使用以下选项:{{{ }}}spring-doc.cadn.net.cn

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

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

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

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

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

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

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

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

考虑以下合同:spring-doc.cadn.net.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())
    }
}
YML
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.cadn.net.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.cadn.net.cn

生成的 WireMock 存根应类似于以下示例:spring-doc.cadn.net.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" ]
  }
}

发送请求,例如request部分合同结果 在发送以下响应正文时:spring-doc.cadn.net.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 的response-template响应转换器。它使用 Handlebars 将 Mustache 模板转换为 适当的值。此外,它还注册了两个辅助函数:{{{ }}}

2.4.6. 匹配器部分中的动态属性

如果您与 Pact 合作,以下讨论可能看起来很熟悉。 相当多的用户习惯于在主体之间进行分离并将 合约的动态部分。spring-doc.cadn.net.cn

您可以使用bodyMatchers部分有两个原因:spring-doc.cadn.net.cn

  • 定义应最终出现在存根中的动态值。 您可以在requestinputMessage合同的一部分。spring-doc.cadn.net.cn

  • 验证测试结果。 此部分位于responseoutputMessage的 合同。spring-doc.cadn.net.cn

目前,Spring Cloud Contract Verifier 仅支持具有 以下匹配可能性:spring-doc.cadn.net.cn

编码 DSL
  • 对于存根(在消费者端的测试中):spring-doc.cadn.net.cn

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

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

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

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

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

  • 对于验证(在生产者端生成的测试中):spring-doc.cadn.net.cn

    • byEquality():从提供的 JSON 路径中生产者的响应中获取的值必须是 等于合同中提供的价值。spring-doc.cadn.net.cn

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

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

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

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

    • byType():从提供的 JSON 路径中生产者的响应中获取的值需要为 与合同中响应正文中定义的类型相同。byType可以采用闭包,您可以在其中设置minOccurrencemaxOccurrence.对于 请求端,您应该使用闭包来断言集合的大小。 这样,就可以断言展平集合的大小。检查 unflattened 集合,请使用自定义方法和byCommand(…​) testMatcher.spring-doc.cadn.net.cn

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

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

YAML
有关 Groovy 的详细说明,请参阅 Groovy 部分 类型意味着什么。

对于 YAML,匹配器的结构类似于以下示例:spring-doc.cadn.net.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.cadn.net.cn

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

以下列表显示了允许的列表type值:spring-doc.cadn.net.cn

您还可以在regexType田。 以下列表显示了允许的正则表达式类型:spring-doc.cadn.net.cn

请考虑以下示例:spring-doc.cadn.net.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}'))))
        }
    }
}
YML
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

在前面的示例中,您可以在matchers部分。对于请求部分,您可以看到,对于所有字段,但valueWithoutAMatcher,存根应包含的正则表达式的值 contain 被显式设置。对于valueWithoutAMatcher,则进行验证 与不使用匹配器的方式相同。在这种情况下,测试将执行 相等性检查。spring-doc.cadn.net.cn

对于响应端bodyMatchers部分,我们在 类似的方式。唯一的区别是byType匹配者也在场。这 验证器引擎检查四个字段,以验证测试的响应是否 具有 JSON 路径与给定字段匹配的值,与 定义,并通过以下检查(基于被调用的方法):spring-doc.cadn.net.cn

  • $.valueWithTypeMatch,发动机检查类型是否相同。spring-doc.cadn.net.cn

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

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

  • $.valueWithMinMax,引擎检查类型并断言大小是否为 在最小和最大出现次数之间。spring-doc.cadn.net.cn

生成的测试类似于以下示例(请注意,一个and部分 将自动生成的断言和断言与匹配器分开):spring-doc.cadn.net.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");
请注意,对于byCommand方法,则示例调用assertThatValueIsANumber.此方法必须在测试基类中定义,或者 静态导入到测试中。请注意,byCommandcall 已转换为assertThatValueIsANumber(parsedJson.read("$.duck"));.这意味着发动机采取了 方法名称,并将正确的 JSON 路径作为参数传递给它。

生成的 WireMock 存根如下所示:spring-doc.cadn.net.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" ]
  }
}
'''
如果您使用matcher,请求和响应的部分matcher地址将从 断言中删除 JSON 路径。在以下情况下 验证集合时,必须为 收集。

请考虑以下示例:spring-doc.cadn.net.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('.+'))
        }
    }
}

前面的代码导致创建以下测试(代码块仅显示断言部分):spring-doc.cadn.net.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(".+")

如您所见,该断言是格式错误的。只有数组的第一个元素得到了 断言。为了解决这个问题,您应该将断言应用于整体$.events集合并使用byCommand(…​)方法。spring-doc.cadn.net.cn

2.5. 异步支持

如果您在服务器端使用异步通信(您的控制器是 返回Callable,DeferredResult,依此类推),然后,在您的合同中,您必须 提供async()方法response部分。以下代码显示了一个示例:spring-doc.cadn.net.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    request {
        method GET()
        url '/get'
    }
    response {
        status OK()
        body 'Passed'
        async()
    }
}
YML
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
        // ...
    }
}

您还可以使用fixedDelayMilliseconds方法或属性来向存根添加延迟。 以下示例显示了如何执行此作:spring-doc.cadn.net.cn

槽的
org.springframework.cloud.contract.spec.Contract.make {
    request {
        method GET()
        url '/get'
    }
    response {
        status 200
        body 'Passed'
        fixedDelayMilliseconds 1000
    }
}
YML
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 正文必须在body元素 作为StringGString.此外,还可以提供身体匹配器 请求和响应。代替jsonPath(…​)方法,则org.springframework.cloud.contract.spec.internal.BodyMatchers.xPath应使用方法,并具有所需的xPath作为第一个参数提供 和适当的MatchingType作为第二。除了byType()被支持。spring-doc.cadn.net.cn

以下示例显示了响应正文中带有 XML 的 Groovy DSL 协定:spring-doc.cadn.net.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())
                            }
                        }
                    }
YML
include::/tmp/releaser-1625584814123-0/spring-cloud-contract/spring-cloud-contract-verifier/src/test/resources/yml/contract_rest_xml.yml
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.cadn.net.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.7. 一个文件中的多个合约

您可以在一个文件中定义多个合同。这样的合同可能类似于 以下示例:spring-doc.cadn.net.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()
        }
    }
]
YML
---
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 {
        // ...
    }
}

在前面的示例中,一个合约具有name字段,另一个则没有。这 导致生成两个测试,它们看起来或多或少如下:spring-doc.cadn.net.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);
    }

}

请注意,对于具有name字段,生成的测试方法名为validate_should_post_a_user.没有name字段称为validate_withList_1.它对应于文件的名称WithList.groovy和 列表中合约的索引。spring-doc.cadn.net.cn

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

should post a user.json
1_WithList.json

第一个文件获得了name参数。第二个获取合约文件的名称(WithList.groovy) 以索引为前缀(在此情况下,合约的索引为1在文件中的合同列表中)。spring-doc.cadn.net.cn

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

2.8. 有状态合约

有状态协定(也称为方案)是应读取的协定定义 挨次。 这在以下情况下可能很有用:spring-doc.cadn.net.cn

  • 您希望以精确定义的顺序执行合约,因为您使用 SpringCloud Contract 来测试您的有状态应用程序spring-doc.cadn.net.cn

我们真的不鼓励你这样做,因为合约测试应该是无状态的。

若要创建有状态合约(或方案),需要 在创建合同时使用正确的命名约定。大会 需要包括订单号,后跟下划线。无论如何,这都有效 无论您是使用 YAML 还是 Groovy。以下列表显示了一个示例:spring-doc.cadn.net.cn

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

这样的树导致 Spring Cloud Contract Verifier 生成 WireMock 的场景,其中包含 名称scenario1以及以下三个步骤:spring-doc.cadn.net.cn

  1. login,标记为Started指向...spring-doc.cadn.net.cn

  2. showCart,标记为Step1指向...spring-doc.cadn.net.cn

  3. 注销,标记为Step2(这结束了场景)。spring-doc.cadn.net.cn

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

3. 集成

3.1. JAX-RS

Spring Cloud Contract 支持 JAX-RS 2 客户端 API。基类需要 定义protected WebTarget webTarget和服务器初始化。唯一的选择 测试 JAX-RS API 是启动一个 Web 服务器。此外,带有正文的请求需要有一个 内容类型。否则,默认值为application/octet-stream被使用。spring-doc.cadn.net.cn

为了使用 JAX-RS 模式,请使用以下设置:spring-doc.cadn.net.cn

testMode = 'JAXRSCLIENT'

以下示例显示了生成的测试 API:spring-doc.cadn.net.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.*;

@SuppressWarnings("rawtypes")
public class FooTest {
\tWebTarget webTarget;

\t@Test
\tpublic void validate_() throws Exception {

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

\t\t// then:
\t\t\tassertThat(response.getStatus()).isEqualTo(200);

\t\t// and:
\t\t\tDocumentContext parsedJson = JsonPath.parse(responseAsString);
\t\t\tassertThatJson(parsedJson).field("['property1']").isEqualTo("a");
\t}

}

"""

3.2. WebFlux 与 WebTestClient

您可以使用 WebTestClient 使用 WebFlux。以下列表显示了如何 将 WebTestClient 配置为测试模式:spring-doc.cadn.net.cn

专家
<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 基类和 RestAssured 对于 WebFlux:spring-doc.cadn.net.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));
    }
}
}
WebTestClientmode 比EXPLICIT模式。

3.3. 具有显式模式的 WebFlux

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

专家
<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.cadn.net.cn

@RunWith(SpringRunner.class)
@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-doc.cadn.net.cn

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

专家
<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.cadn.net.cn

考虑以下合同:spring-doc.cadn.net.cn

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

以下示例演示如何设置基类和 RestAssured:spring-doc.cadn.net.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.cadn.net.cn

  • 自动生成的测试中的所有请求都会发送到真实端点,并带有 上下文路径(例如,/my-context-path/url).spring-doc.cadn.net.cn

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

3.5. 使用 REST 文档

您可以使用 Spring REST Docs 生成 文档(例如,Asciidoc 格式)用于使用 Spring MockMvc 的 HTTP API,WebTestClient,或 RestAssured。在为 API 生成文档的同时,您还可以 使用 Spring Cloud Contract WireMock 生成 WireMock 存根。为此,请将您的 普通 REST Docs 测试用例和使用@AutoConfigureRestDocs要有存根 在 REST Docs 输出目录中自动生成。spring-doc.cadn.net.cn

REST 文档

以下示例使用MockMvc:spring-doc.cadn.net.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"));
    }
}

此测试在target/snippets/stubs/resource.json.它匹配 都GET请求/resource路径。相同的示例WebTestClient(用过 用于测试 Spring WebFlux 应用程序)将如下所示:spring-doc.cadn.net.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 方法和除hostcontent-length.要匹配 更精确地请求(例如,为了匹配 POST 或 PUT 的正文),我们需要 显式创建请求匹配器。这样做有两个效果:spring-doc.cadn.net.cn

此功能的主要入口点是WireMockRestDocs.verify(),可以使用 作为document()方便的方法,如下所示 示例显示:spring-doc.cadn.net.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"));
    }
}

前面的合约指定任何具有id字段接收响应 在此测试中定义。您可以将调用链接在一起.jsonPath()添加其他 匹配器。如果 JSON 路径不熟悉,则 JayWay 文档可以帮助您快速上手。这WebTestClient此测试的版本 有类似的verify()插入同一位置的静态帮助程序。spring-doc.cadn.net.cn

而不是jsonPathcontentType方便的方法,也可以使用 WireMock API 来验证请求是否与创建的存根匹配,作为 以下示例显示:spring-doc.cadn.net.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 很丰富。您可以通过以下方式匹配标头、查询参数和请求正文 正则表达式以及 JSON 路径。您可以使用这些功能创建具有更宽的存根 参数范围。前面的示例生成了一个类似于以下示例的存根:spring-doc.cadn.net.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"
    }
  }
}
您可以使用wiremock()方法或jsonPath()contentType()方法来创建请求匹配器,但不能同时使用这两种方法。

在消费者方面,您可以将resource.json在本节前面生成 在类路径上可用(例如,通过将存根发布为 JAR)。之后,您可以创建一个在 多种不同的方式,包括使用@AutoConfigureWireMock(stubs="classpath:resource.json"),如本文前面所述 公文。spring-doc.cadn.net.cn

3.5.1. 使用 REST 文档生成合约

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

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

您可能想知道为什么此功能位于 WireMock 模块中。功能 是因为同时生成合约和存根是有意义的。

考虑以下测试:spring-doc.cadn.net.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.cadn.net.cn

合约称为index.groovy,可能类似于以下示例:spring-doc.cadn.net.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.adoc.spring-doc.cadn.net.cn

4. 消息传递

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

4.1. 消息传递 DSL 顶级元素

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

4.1.1. 由方法触发的输出

可以通过调用方法(例如Scheduler当合同是 已启动并发送了一条消息),如以下示例所示:spring-doc.cadn.net.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')
        }
    }
}
YML
# 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

在前面的示例案例中,输出消息被发送到output如果调用bookReturnedTriggered被执行。在消息发布者端,我们生成一个 调用该方法以触发消息的测试。在消费者方面,您可以使用 这some_label以触发消息。spring-doc.cadn.net.cn

4.1.2. 消息触发的输出

输出消息可以通过接收消息来触发,如下图所示 例:spring-doc.cadn.net.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')
        }
    }
}
YML
# 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

在前面的示例中,输出消息被发送到output如果正确的消息是 在input目的地。在消息发布者端,引擎 生成一个测试,将输入消息发送到定义的目标。在 消费者方面,您可以向输入目的地发送消息或使用标签 (some_label在示例中)触发消息。spring-doc.cadn.net.cn

4.1.3. 消费者/生产者

此部分仅对 Groovy DSL 有效。

在 HTTP 中,你有一个概念client/stub and `server/test表示法。您还可以 在消息传递中使用这些范式。此外,Spring Cloud Contract Verifier 还 提供consumerproducer方法,如以下示例所示 (请注意,您可以使用$value提供的方法consumerproducer零件):spring-doc.cadn.net.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. 普通

inputoutputMessage部分,您可以调用assertThat与名称 的method(例如,assertThatMessageIsOnTheQueue()) 您在 基类或静态导入。Spring Cloud Contract 运行该方法 在生成的测试中。spring-doc.cadn.net.cn

4.2. 集成

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

由于我们使用 Spring Boot,如果您已将其中一个库添加到类路径中,则所有 消息传递配置将自动设置。spring-doc.cadn.net.cn

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

如果要使用 Spring Cloud Stream,请记住添加一个依赖org.springframework.cloud:spring-cloud-stream-test-support如下:spring-doc.cadn.net.cn

专家
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream-test-support</artifactId>
    <scope>test</scope>
</dependency>
Gradle
testCompile "org.springframework.cloud:spring-cloud-stream-test-support"

4.2.1. 手动集成测试

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

在测试中,您可以注入一个ContractVerifierMessageExchange发送和接收合同后面的消息。然后添加@AutoConfigureMessageVerifier到你的测试中。以下示例显示了如何执行此作:spring-doc.cadn.net.cn

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

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

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

拥有inputoutputMessage部分会导致创建测试在发布者方面。默认情况下,会创建 JUnit 4 测试。但是,还有一个可以创建 JUnit 5、TestNG 或 Spock 测试。spring-doc.cadn.net.cn

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

传递给messageFromsentTo可以有不同的不同消息传递实现的含义。对于流和集成,它是首先解析为destination通道的。那么,如果没有这样的destination它被解析为通道名称。对于 Camel,这是一个特定的组件(例如,jms).

4.3.1. 场景 1:无输入消息

考虑以下合同:spring-doc.cadn.net.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())
        }
    }
}
YML
label: some_label
input:
  triggeredBy: bookReturnedTriggered
outputMessage:
  sentTo: activemq:output
  body:
    bookName: foo
  headers:
    BOOK-NAME: foo
    contentType: application/json

对于前面的示例,将创建以下测试:spring-doc.cadn.net.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;

@SuppressWarnings("rawtypes")
public class FooTest {
\t@Inject ContractVerifierMessaging contractVerifierMessaging;
\t@Inject ContractVerifierObjectMapper contractVerifierObjectMapper;

\t@Test
\tpublic void validate_foo() throws Exception {
\t\t// when:
\t\t\tbookReturnedTriggered();

\t\t// then:
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output");
\t\t\tassertThat(response).isNotNull();

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

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

}

'''
斯波克
                    '''\
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

@SuppressWarnings("rawtypes")
class FooSpec extends Specification {
\t@Inject ContractVerifierMessaging contractVerifierMessaging
\t@Inject ContractVerifierObjectMapper contractVerifierObjectMapper

\tdef validate_foo() throws Exception {
\t\twhen:
\t\t\tbookReturnedTriggered()

\t\tthen:
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output")
\t\t\tresponse != null

\t\tand:
\t\t\tresponse.getHeader("BOOK-NAME") != null
\t\t\tresponse.getHeader("BOOK-NAME").toString() == 'foo'
\t\t\tresponse.getHeader("contentType") != null
\t\t\tresponse.getHeader("contentType").toString() == 'application/json'

\t\tand:
\t\t\tDocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
\t\t\tassertThatJson(parsedJson).field("['bookName']").isEqualTo("foo")
\t}

}

'''

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

考虑以下合同:spring-doc.cadn.net.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')
        }
    }
}
YML
label: some_label
input:
  messageFrom: jms:input
  messageBody:
    bookName: 'foo'
  messageHeaders:
    sample: header
outputMessage:
  sentTo: jms:output
  body:
    bookName: foo
  headers:
    BOOK-NAME: foo

对于前面的合同,将创建以下测试:spring-doc.cadn.net.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;

@SuppressWarnings("rawtypes")
public class FooTest {
\t@Inject ContractVerifierMessaging contractVerifierMessaging;
\t@Inject ContractVerifierObjectMapper contractVerifierObjectMapper;

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

\t\t// when:
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:input");

\t\t// then:
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("jms:output");
\t\t\tassertThat(response).isNotNull();

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

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

}

'''
斯波克
                    """\
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

@SuppressWarnings("rawtypes")
class FooSpec extends Specification {
\t@Inject ContractVerifierMessaging contractVerifierMessaging
\t@Inject ContractVerifierObjectMapper contractVerifierObjectMapper

\tdef validate_foo() throws Exception {
\t\tgiven:
\t\t\tContractVerifierMessage inputMessage = contractVerifierMessaging.create(
\t\t\t\t\t'''{"bookName":"foo"}'''
\t\t\t\t\t\t, headers()
\t\t\t\t\t\t\t.header("sample", "header")
\t\t\t)

\t\twhen:
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:input")

\t\tthen:
\t\t\tContractVerifierMessage response = contractVerifierMessaging.receive("jms:output")
\t\t\tresponse != null

\t\tand:
\t\t\tresponse.getHeader("BOOK-NAME") != null
\t\t\tresponse.getHeader("BOOK-NAME").toString() == 'foo'

\t\tand:
\t\t\tDocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
\t\t\tassertThatJson(parsedJson).field("['bookName']").isEqualTo("foo")
\t}

}

"""

4.3.3. 场景 3:无输出消息

考虑以下合同:spring-doc.cadn.net.cn

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

对于前面的合同,将创建以下测试:spring-doc.cadn.net.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;

@SuppressWarnings("rawtypes")
public class FooTest {
\t@Inject ContractVerifierMessaging contractVerifierMessaging;
\t@Inject ContractVerifierObjectMapper contractVerifierObjectMapper;

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

\t\t// when:
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:delete");
\t\t\tbookWasDeleted();

\t}

}

"""
斯波克
                    """\
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

@SuppressWarnings("rawtypes")
class FooSpec extends Specification {
\t@Inject ContractVerifierMessaging contractVerifierMessaging
\t@Inject ContractVerifierObjectMapper contractVerifierObjectMapper

\tdef validate_foo() throws Exception {
\t\tgiven:
\t\t\tContractVerifierMessage inputMessage = contractVerifierMessaging.create(
\t\t\t\t\t'''{"bookName":"foo"}'''
\t\t\t\t\t\t, headers()
\t\t\t\t\t\t\t.header("sample", "header")
\t\t\t)

\t\twhen:
\t\t\tcontractVerifierMessaging.send(inputMessage, "jms:delete")
\t\t\tbookWasDeleted()

\t\tthen:
\t\t\tnoExceptionThrown()
\t}

}
"""

4.4. 消费者存根生成

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

如果类路径上有多个框架,则存根运行程序需要 定义应该使用哪一个。假设您有 AMQP、Spring Cloud Stream 和 Spring Integration 在类路径上,并且您想要使用 Spring AMQP。然后你需要设置stubrunner.stream.enabled=falsestubrunner.integration.enabled=false. 这样,唯一剩下的框架就是 Spring AMQP。

4.4.1. 存根触发

要触发消息,请使用StubTrigger接口,如以下示例所示:spring-doc.cadn.net.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();

}

为方便起见,该StubFinder接口扩展StubTrigger,所以你只需要一个 或测试中的另一个。spring-doc.cadn.net.cn

StubTrigger为您提供以下触发消息的选项:spring-doc.cadn.net.cn

4.4.2. 按标签触发

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

stubFinder.trigger('return_book_1')

4.4.3. 按组和工件 ID 触发

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

4.4.4. 由工件 ID 触发

以下示例演示如何从项目 ID 触发消息:spring-doc.cadn.net.cn

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

4.4.5. 触发所有消息

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

stubFinder.trigger()

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

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

4.5.1. 将 Apache Camel 添加到项目中

您可以在类路径上同时使用 Apache Camel 和 Spring Cloud Contract Stub Runner。请记住使用@AutoConfigureStubRunner.spring-doc.cadn.net.cn

4.5.2. 禁用功能

如果您需要禁用此功能,请将stubrunner.camel.enabled=false财产。spring-doc.cadn.net.cn

4.5.3. 示例

假设我们有以下 Maven 存储库,其中包含用于camelService应用。spring-doc.cadn.net.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.cadn.net.cn

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

现在考虑以下合约(我们将它们编号为 1 和 2):spring-doc.cadn.net.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')
        }
    }
}
场景 1(无输入消息)

要从return_book_1标签,我们使用StubTrigger接口,如下所示:spring-doc.cadn.net.cn

stubFinder.trigger('return_book_1')

接下来,我们要监听发送到jms:output:spring-doc.cadn.net.cn

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

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

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

由于路线是为您设置的,您可以向jms:output目的地。spring-doc.cadn.net.cn

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

接下来,我们要监听发送到jms:output如下:spring-doc.cadn.net.cn

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

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

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

由于路线是为您设置的,您可以向jms:outputdestination,如下所示:spring-doc.cadn.net.cn

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

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

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

4.6.1. 将运行器添加到项目中

您可以在 类路径。请记住使用@AutoConfigureStubRunner.spring-doc.cadn.net.cn

4.6.2. 禁用功能

如果您需要禁用此功能,请将stubrunner.integration.enabled=false财产。spring-doc.cadn.net.cn

4.6.3. 示例

假设您有以下 Maven 存储库,其中包含用于integrationService应用:spring-doc.cadn.net.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.cadn.net.cn

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

考虑以下合同(编号为 1 和 2):spring-doc.cadn.net.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 Integration 路线:spring-doc.cadn.net.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.cadn.net.cn

场景 1(无输入消息)

要从return_book_1标签,请使用StubTrigger接口,作为 遵循:spring-doc.cadn.net.cn

stubFinder.trigger('return_book_1')

以下列表显示了如何侦听发送到jms:output:spring-doc.cadn.net.cn

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

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

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

由于路线是为您设置的,您可以向jms:outputdestination,如下所示:spring-doc.cadn.net.cn

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

以下列表显示了如何侦听发送到jms:output:spring-doc.cadn.net.cn

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

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

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

由于路线是为您设置的,您可以向jms:inputdestination,如下所示:spring-doc.cadn.net.cn

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

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

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

如果存根运行器与流的集成messageFromsentTo字符串 首先解析为destination频道的,没有这样的destination存在,则 destination 被解析为通道名称。

如果要使用 Spring Cloud Stream,请记住添加一个依赖org.springframework.cloud:spring-cloud-stream-test-support如下:spring-doc.cadn.net.cn

专家
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream-test-support</artifactId>
    <scope>test</scope>
</dependency>
Gradle
testCompile "org.springframework.cloud:spring-cloud-stream-test-support"

4.7.1. 将运行器添加到项目中

您可以在 类路径。请记住使用@AutoConfigureStubRunner.spring-doc.cadn.net.cn

4.7.2. 禁用该功能

如果您需要禁用此功能,请将stubrunner.stream.enabled=false财产。spring-doc.cadn.net.cn

4.7.3. 示例

假设您有以下 Maven 存储库,其中包含用于streamService应用:spring-doc.cadn.net.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.cadn.net.cn

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

考虑以下合同(编号为 1 和 2):spring-doc.cadn.net.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.cadn.net.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.cadn.net.cn

场景 1(无输入消息)

要从return_book_1标签,请使用StubTriggerinterface 作为 遵循:spring-doc.cadn.net.cn

stubFinder.trigger('return_book_1')

以下示例显示如何监听发送到该通道的消息的输出,该通道destinationreturnBook:spring-doc.cadn.net.cn

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

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

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

由于路线是为您设置的,您可以向bookStorage destination如下:spring-doc.cadn.net.cn

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

以下示例演示如何监听发送到returnBook:spring-doc.cadn.net.cn

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

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

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

由于路线是为您设置的,您可以向jms:outputdestination,如下所示:spring-doc.cadn.net.cn

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

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

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

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

在消息使用者端,存根运行器将所有@RabbitListener注释 端点和所有SimpleMessageListenerContainer应用程序上下文中的对象。spring-doc.cadn.net.cn

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

如果您需要使用路由键,可以使用amqp_receivedRoutingKeymessaging 标头。spring-doc.cadn.net.cn

4.8.1. 将运行器添加到项目中

您可以在类路径上同时拥有 Spring AMQP 和 Spring Cloud Contract Stub Runner,并且 设置属性stubrunner.amqp.enabled=true.记得注释你的测试类 跟@AutoConfigureStubRunner.spring-doc.cadn.net.cn

如果类路径上已经有 Stream 和 Integration,则需要 通过设置stubrunner.stream.enabled=falsestubrunner.integration.enabled=false性能。

4.8.2. 示例

假设您有以下 Maven 存储库,其中包含用于spring-cloud-contract-amqp-test应用:spring-doc.cadn.net.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.cadn.net.cn

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

然后考虑以下合同:spring-doc.cadn.net.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.cadn.net.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
触发消息

要使用上一节中的协定触发消息,请使用StubTriggerinterface 作为 遵循:spring-doc.cadn.net.cn

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

消息的目的地为contract-test.exchange,因此 Spring AMQP 存根运行器 集成查找与此交换相关的绑定,如以下示例所示:spring-doc.cadn.net.cn

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

绑定定义绑定名为test.queue.因此,以下侦听器 定义与合约消息匹配并调用:spring-doc.cadn.net.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.cadn.net.cn

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

为了避免 Spring AMQP 在我们的测试期间尝试连接到正在运行的代理,我们 配置模拟ConnectionFactory.spring-doc.cadn.net.cn

禁用模拟的ConnectionFactory,请设置以下属性:stubrunner.amqp.mockConnection=false如下:spring-doc.cadn.net.cn

stubrunner:
  amqp:
    mockConnection: false

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

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

集成假设您有一个正在运行的 JMS 代理实例(例如activemq嵌入式代理)。spring-doc.cadn.net.cn

4.9.1. 将运行器添加到项目中

您需要在类路径上同时具有 Spring JMS 和 Spring Cloud Contract Stub Runner。记得注释你的测试类 跟@AutoConfigureStubRunner.spring-doc.cadn.net.cn

4.9.2. 示例

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

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

进一步假设以下测试配置:spring-doc.cadn.net.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.cadn.net.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_1标签,我们使用StubTrigger接口,如下所示:spring-doc.cadn.net.cn

stubFinder.trigger('return_book_1')

接下来,我们要监听发送到output:spring-doc.cadn.net.cn

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

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

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

由于路线是为您设置的,您可以向output目的地。spring-doc.cadn.net.cn

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

接下来,我们要监听发送到output如下:spring-doc.cadn.net.cn

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

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

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

由于路线是为您设置的,您可以向outputdestination,如下所示:spring-doc.cadn.net.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.cadn.net.cn

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

4.10.1. 将运行器添加到项目中

您需要同时拥有 Spring Kafka、Spring Kafka 测试(以运行@EmbeddedBroker)和 Spring Cloud Contract Stub Runner 在类路径上。记得注释你的测试类 跟@AutoConfigureStubRunner.spring-doc.cadn.net.cn

使用 Kafka 集成,为了轮询单个消息,我们需要在 Spring 上下文启动时注册一个消费者。这可能会导致这样一种情况,即当你在使用者端时,Stub Runner 可以为同一组 ID 和主题注册一个额外的使用者。这可能导致只有一个组件实际轮询消息的情况。由于在消费者端,您同时拥有 Spring Cloud Contract Stub Runner 和 Spring Cloud Contract Verifier 类路径,因此我们需要能够关闭此类行为。这是通过stubrunner.kafka.initializer.enabled标志,这将禁用联系人验证程序使用者注册。如果您的应用程序既是 kafka 消息的使用者又是生产者,您可能需要手动将该属性切换为false在生成的测试的基类中。spring-doc.cadn.net.cn

4.10.2. 示例

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

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

进一步假设以下测试配置(请注意spring.kafka.bootstrap-servers通过以下方式指向嵌入式代理的 IP${spring.embedded.kafka.brokers}):spring-doc.cadn.net.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
如果您的应用程序使用非整数记录键,则需要将spring.kafka.producer.key-serializerspring.kafka.consumer.key-deserializer属性,因为 Kafka de/serialization 需要非 null 将键记录为整数类型。

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

场景 1(无输入消息)

要从return_book_1标签,我们使用StubTrigger接口,如下所示:spring-doc.cadn.net.cn

stubFinder.trigger('return_book_1')

接下来,我们要监听发送到output:spring-doc.cadn.net.cn

Message receivedMessage = receiveFromOutput()

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

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

由于路线是为您设置的,您可以向output目的地。spring-doc.cadn.net.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)

接下来,我们要监听发送到output如下:spring-doc.cadn.net.cn

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

收到的消息将传递以下断言:spring-doc.cadn.net.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(输入无输出)

由于路线是为您设置的,您可以向outputdestination,如下所示:spring-doc.cadn.net.cn

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

5. Spring Cloud 合约存根运行器

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

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

5.1. 快照版本

您可以将其他快照存储库添加到build.gradle要使用快照的文件 版本,每次成功构建后自动上传,如下所示:spring-doc.cadn.net.cn

专家
<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
/*
 We need to use the [buildscript {}] section when we have to modify
 the classpath for the plugins. If that's not the case this section
 can be skipped.

 If you don't need to modify the classpath (e.g. add a Pact dependency),
 then you can just set the [pluginManagement {}] section in [settings.gradle] file.

 // settings.gradle
 pluginManagement {
    repositories {
        // for snapshots
        maven {url "https://repo.spring.io/snapshot"}
        // for milestones
        maven {url "https://repo.spring.io/milestone"}
        // for GA versions
        gradlePluginPortal()
    }
 }

 */
buildscript {
    repositories {
        mavenCentral()
        mavenLocal()
        maven { url "https://repo.spring.io/snapshot" }
        maven { url "https://repo.spring.io/milestone" }
        maven { url "https://repo.spring.io/release" }
    }

5.2. 将存根发布为 JAR

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

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

以下示例显示了如何将存根发布为 jar:spring-doc.cadn.net.cn

专家
<!-- 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>
                    ${basedir}/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>${basedir}/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
// the presented publication is also added by the plugin but you can modify it as you wish

publishing {
    publications {
        stubs(MavenPublication) {
            artifactId "${project.name}-stubs"
            artifact verifierStubsJar
        }
    }
}

5.3. 存根流道核心

存根运行器核心为服务协作者运行存根。将存根视为 服务允许您使用 stub-runner 作为消费者驱动的合约的实现。spring-doc.cadn.net.cn

Stub Runner 允许您自动下载所提供依赖项的存根(或 从类路径中选择它们),为它们启动 WireMock 服务器,并为它们提供适当的 存根定义。对于消息传递,定义了特殊的存根路由。spring-doc.cadn.net.cn

5.3.1. 检索存根

您可以从以下获取存根的选项中进行选择:spring-doc.cadn.net.cn

  • 基于以太的解决方案,可从 Artifactory 或 Nexus 下载带有存根的 JAR。spring-doc.cadn.net.cn

  • 类路径扫描解决方案,使用模式搜索类路径以检索存根spring-doc.cadn.net.cn

  • 编写您自己的org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder用于完全定制spring-doc.cadn.net.cn

后一个示例在“自定义存根运行器”部分中进行了描述。spring-doc.cadn.net.cn

下载存根

您可以使用stubsMode开关。它从StubRunnerProperties.StubsMode列举。您可以使用以下选项:spring-doc.cadn.net.cn

以下示例从本地位置选取存根:spring-doc.cadn.net.cn

@AutoConfigureStubRunner(repositoryRoot="https://foo.bar", ids = "com.example:beer-api-producer:+:stubs:8095", stubsMode = StubRunnerProperties.StubsMode.LOCAL)
类路径扫描

如果您将stubsMode属性设置为StubRunnerProperties.StubsMode.CLASSPATH(或从以下原因开始设置任何内容CLASSPATH是默认值),则扫描类路径。 请考虑以下示例:spring-doc.cadn.net.cn

@AutoConfigureStubRunner(ids = {
    "com.example:beer-api-producer:+:stubs:8095",
    "com.example.foo:bar:1.0.0:superstubs:8096"
})

您可以将依赖项添加到类路径中,如下所示:spring-doc.cadn.net.cn

专家
<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
}

然后扫描类路径上的指定位置。为com.example:beer-api-producer-restdocs, 扫描以下位置:spring-doc.cadn.net.cn

com.example.thing1:thing2,扫描以下位置:spring-doc.cadn.net.cn

打包 生产者存根。

为了实现适当的存根包装,生产商将按如下方式设置合同:spring-doc.cadn.net.cn

└── src
    └── test
        └── resources
            └── contracts
                └── com.example
                    └── beer-api-producer-restdocs
                        └── nested
                            └── contract3.groovy

通过使用专家assembly插件Gradle Jar 任务,您必须创建以下内容 存根罐中的结构:spring-doc.cadn.net.cn

└── META-INF
    └── com.example
        └── beer-api-producer-restdocs
            └── 2.0.0
                ├── contracts
                │   └── nested
                │       └── contract2.groovy
                └── mappings
                    └── mapping.json

通过维护这种结构,类路径将被扫描,您可以从消息传递或 HTTP 存根,无需下载工件。spring-doc.cadn.net.cn

配置 HTTP 服务器存根

存根运行器有一个概念HttpServerStub抽象了底层 HTTP 服务器的具体实现(例如,WireMock 是实现之一)。 有时,您需要对存根服务器执行一些额外的调整(这对于给定的实现是具体的)。 为此,Stub Runner 为您提供了 这httpServerStubConfigurer属性和 JUnit 规则,可通过系统属性访问,您可以在其中提供 您对org.springframework.cloud.contract.stubrunner.HttpServerStubConfigurer接口。实现可以更改 给定 HTTP 服务器存根的配置文件。spring-doc.cadn.net.cn

Spring Cloud Contract Stub Runner 附带了一个实现,您可以 可以扩展为 WireMock:org.springframework.cloud.contract.stubrunner.provider.wiremock.WireMockHttpServerStubConfigurer. 在configure方法 您可以为给定的存根提供自己的自定义配置。用途 case 可能会在 HTTPS 端口上为给定的工件 ID 启动 WireMock。以下内容 示例显示了如何执行此作:spring-doc.cadn.net.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
    }
}

然后,您可以将其与@AutoConfigureStubRunner注释,如下所示:spring-doc.cadn.net.cn

@AutoConfigureStubRunner(mappingsOutputFolder = "target/outputmappings/",
        httpServerStubConfigurer = HttpsForFraudDetection)

每当找到 HTTPS 端口时,它都会优先于 HTTP 端口。spring-doc.cadn.net.cn

5.3.2. 运行存根

本节介绍如何运行存根。它包含以下主题:spring-doc.cadn.net.cn

HTTP 存根

存根在 JSON 文档中定义,其语法在 WireMock 文档中定义spring-doc.cadn.net.cn

以下示例在 JSON 中定义存根:spring-doc.cadn.net.cn

{
    "request": {
        "method": "GET",
        "url": "/ping"
    },
    "response": {
        "status": 200,
        "body": "pong",
        "headers": {
            "Content-Type": "text/plain"
        }
    }
}
查看已注册的映射

每个存根协作者都会在__/admin/端点。spring-doc.cadn.net.cn

您还可以使用mappingsOutputFolder属性将映射转储到文件。 对于基于注释的方法,它类似于以下示例:spring-doc.cadn.net.cn

@AutoConfigureStubRunner(ids="a.b.c:loanIssuance,a.b.c:fraudDetectionServer",
mappingsOutputFolder = "target/outputmappings/")

对于 JUnit 方法,它类似于以下示例:spring-doc.cadn.net.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/outputmappings文件夹,你会看到以下结构;spring-doc.cadn.net.cn

.
├── fraudDetectionServer_13705
└── loanIssuance_12255

这意味着注册了两个存根。fraudDetectionServer在端口注册13705loanIssuance在端口12255.如果我们看一下其中一个文件,我们会看到(对于 WireMock) 给定服务器可用的映射:spring-doc.cadn.net.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.cadn.net.cn

5.4. 存根运行器 JUnit 规则和存根运行器 JUnit5 扩展

Stub Runner 带有一个 JUnit 规则,可让您下载和运行给定的存根 组和项目 ID,如以下示例所示:spring-doc.cadn.net.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");
}

一个StubRunnerExtension也可用于 JUnit 5。StubRunnerRuleStubRunnerExtension以非常相似的方式工作。规则或扩展名 执行时,Stub Runner 连接到您的 Maven 存储库,并且对于给定的列表 dependencies,尝试:spring-doc.cadn.net.cn

Stub Runner 使用 Eclipse Aether 机制下载 Maven 依赖项。 查看他们的文档以获取更多信息。spring-doc.cadn.net.cn

由于StubRunnerRuleStubRunnerExtension实现StubFinder他们让你找到开始的存根,如以下示例所示:spring-doc.cadn.net.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.cadn.net.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()
}
6月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");
}
JUnit 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 "";
    }
}

有关如何应用 Stub Runner 的全局配置。spring-doc.cadn.net.cn

要将 JUnit 规则或 JUnit 5 扩展与消息传递一起使用,您必须提供MessageVerifier接口到规则构建器(例如,rule.messageVerifier(new MyMessageVerifier())). 如果不这样做,则每当您尝试发送消息时,都会抛出异常。

5.4.1. Maven 设置

存根下载器遵循不同本地存储库文件夹的 Maven 设置。 当前不考虑存储库和配置文件的身份验证详细信息, 因此,您需要使用上面提到的属性来指定它。spring-doc.cadn.net.cn

5.4.2. 提供固定端口

您还可以在固定端口上运行存根。您可以通过两种不同的方式进行作。 一种是在属性中传递它,另一种是使用 JUnit 规则。spring-doc.cadn.net.cn

5.4.3. 流畅的 API

使用StubRunnerRuleStubRunnerExtension,您可以添加一个存根进行下载 然后传递上次下载存根的端口。以下示例显示了如何执行此作:spring-doc.cadn.net.cn

@ClassRule
public static StubRunnerRule rule = new StubRunnerRule().repoRoot(repoRoot())
        .stubsMode(StubRunnerProperties.StubsMode.REMOTE)
        .downloadStub("org.springframework.cloud.contract.verifier.stubs",
                "loanIssuance")
        .withPort(12345).downloadStub(
                "org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer:12346");

@BeforeClass
@AfterClass
public static void setupProps() {
    System.clearProperty("stubrunner.repository.root");
    System.clearProperty("stubrunner.classifier");
}

对于前面的示例,以下测试是有效的:spring-doc.cadn.net.cn

then(rule.findStubUrl("loanIssuance"))
        .isEqualTo(URI.create("http://localhost:12345").toURL());
then(rule.findStubUrl("fraudDetectionServer"))
        .isEqualTo(URI.create("http://localhost:12346").toURL());

5.4.4. 带弹簧的短管流道

Stub Runner with Spring 设置 Stub Runner 项目的 Spring 配置。spring-doc.cadn.net.cn

通过在配置文件中提供存根列表,Stub Runner 会自动下载 并在 WireMock 中注册选定的存根。spring-doc.cadn.net.cn

如果您想查找存根依赖项的 URL,您可以自动连接StubFinder接口和使用 其方法如下:spring-doc.cadn.net.cn

@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
@SpringBootTest(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

    @BeforeClass
    @AfterClass
    void setupProps() {
        System.clearProperty("stubrunner.repository.root")
        System.clearProperty("stubrunner.classifier")
        WireMockHttpServerStubAccessor.clear()
    }

    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.cadn.net.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

除了使用属性,您还可以使用@AutoConfigureStubRunner. 以下示例通过在注释上设置值来实现相同的结果:spring-doc.cadn.net.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 服务器。以下示例显示了com.example:thing1com.example:thing2:spring-doc.cadn.net.cn

可以在代码中引用这些值。spring-doc.cadn.net.cn

您还可以使用@StubRunnerPort注释以注入正在运行的存根的端口。 注释的值可以是groupid:artifactid或者只是artifactid. 以下示例显示了com.example:thing1com.example:thing2.spring-doc.cadn.net.cn

@StubRunnerPort("thing1")
int thing1Port;
@StubRunnerPort("com.example:thing2")
int thing2Port;

5.5. 存根运行器 Spring Cloud

Stub Runner 可以与 Spring Cloud 集成。spring-doc.cadn.net.cn

有关现实生活中的示例,请参阅:spring-doc.cadn.net.cn

5.5.1. 存根服务发现

最重要的特点Stub Runner Spring Cloud是它存根的事实:spring-doc.cadn.net.cn

这意味着,无论您使用 Zookeeper、Consul、Eureka 还是其他任何东西 否则,您在测试中不需要它。我们正在启动您的 WireMock 实例 依赖项,并且无论何时您使用Feign,加载 平衡RestTemplateDiscoveryClient直接调用那些存根服务器 而不是调用真正的服务发现工具。spring-doc.cadn.net.cn

例如,以下测试通过:spring-doc.cadn.net.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.cadn.net.cn

stubrunner:
  idsToServiceIds:
    ivyNotation: someValueInsideYourCode
    fraudDetectionServer: someNameThatShouldMapFraudDetectionServer
测试配置文件和服务发现

在集成测试中,您通常不希望调用任何一个发现服务(例如 Eureka) 或配置服务器。这就是创建要禁用的其他测试配置的原因 这些功能。spring-doc.cadn.net.cn

由于某些限制spring-cloud-commons, 为此,您必须禁用这些属性 在静态块中,例如以下示例(对于 Eureka):spring-doc.cadn.net.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. 附加配置

您可以匹配artifactId存根的存根,使用应用程序的名称stubrunner.idsToServiceIds:地图。 您可以通过设置stubrunner.cloud.ribbon.enabledfalse您可以通过设置stubrunner.cloud.enabledfalsespring-doc.cadn.net.cn

默认情况下,所有服务发现都是存根的。这意味着,无论您是否拥有 现有的DiscoveryClient,则忽略其结果。但是,如果您想重复使用它,您可以将stubrunner.cloud.delegate.enabledtrue,然后是现有的DiscoveryClient结果是 与存根合并。

Stub Runner 使用的默认 Maven 配置可以进行调整 通过设置以下系统属性或设置相应的环境变量:spring-doc.cadn.net.cn

5.6. 使用 Stub Runner Boot 应用程序

Spring Cloud Contract Stub Runner Boot 是一个 Spring Boot 应用程序,它将 REST 端点公开给触发消息传递标签并访问 WireMock 服务器。spring-doc.cadn.net.cn

其中一个用例是在已部署的应用程序上运行一些烟雾(端到端)测试。您可以查看 Spring Cloud Pipelines 项目了解更多信息。spring-doc.cadn.net.cn

5.6.1. 存根运行器服务器

要使用 Stub Runner Server,请添加以下依赖项:spring-doc.cadn.net.cn

compile "org.springframework.cloud:spring-cloud-starter-stub-runner"

然后用@EnableStubRunnerServer,建造一个肥大的罐子,它就可以工作了。spring-doc.cadn.net.cn

有关属性,请参阅短截线流道弹簧部分。spring-doc.cadn.net.cn

5.6.2. 存根运行器服务器脂肪罐

您可以从 Maven 下载独立的 JAR(例如,对于版本 2.0.1.RELEASE)通过运行以下命令:spring-doc.cadn.net.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.3. Spring Cloud CLI

1.4.0.RELEASESpring Cloud CLI 项目版本,您可以通过运行spring cloud stubrunner.spring-doc.cadn.net.cn

为了传递配置,您可以创建一个stubrunner.yml当前工作目录中的文件,在名为config,或在~/.spring-cloud.该文件可能类似于以下内容 运行本地安装的存根的示例:spring-doc.cadn.net.cn

示例 2.stubrunner.yml
stubrunner:
  stubsMode: LOCAL
  ids:
    - com.example:beer-api-producer:+:9876

然后你可以调用spring cloud stubrunner从终端窗口开始 存根运行器服务器。在端口可用8750.spring-doc.cadn.net.cn

5.6.4. 端点

Stub Runner Boot 提供两个端点:spring-doc.cadn.net.cn

HTTP

对于 HTTP,存根运行器启动使以下端点可用:spring-doc.cadn.net.cn

消息

对于消息传递,存根运行器启动使以下端点可用:spring-doc.cadn.net.cn

5.6.5. 示例

以下示例显示了 Stub Runner Boot 的典型用法:spring-doc.cadn.net.cn

@ContextConfiguration(classes = StubRunnerBoot, loader = SpringBootContextLoader)
@SpringBootTest(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.6. 使用服务发现启动存根运行器

使用 Stub Runner Boot 的一种方法是将其用作“冒烟测试”的存根源。那是什么意思? 假设您不想按顺序将 50 个微服务部署到测试环境 以查看您的应用程序是否有效。在构建过程中,您已经执行了一套测试, 但您还希望确保应用程序的打包有效。您可以 将应用程序部署到环境,启动它,然后对其运行几个测试,看看是否 它有效。我们可以将这些测试称为“烟雾测试”,因为它们的目的只是检查少数几个 测试场景。spring-doc.cadn.net.cn

这种方法的问题在于,如果您使用微服务,您很可能还会 使用服务发现工具。Stub Runner Boot 允许您通过启动 必需的存根,并在服务发现工具中注册它们。考虑以下示例 使用 Eureka 进行这样的设置(假设 Eureka 已经在运行):spring-doc.cadn.net.cn

@SpringBootApplication
@EnableStubRunnerServer
@EnableEurekaClient
@AutoConfigureStubRunner
public class StubRunnerBootEurekaExample {

    public static void main(String[] args) {
        SpringApplication.run(StubRunnerBootEurekaExample.class, args);
    }

}

我们想要启动一个存根运行器启动服务器 (@EnableStubRunnerServer),启用 Eureka 客户端 (@EnableEurekaClient), 并打开存根运行器功能 (@AutoConfigureStubRunner).spring-doc.cadn.net.cn

现在假设我们要启动此应用程序,以便自动注册存根。 我们可以通过使用java -jar ${SYSTEM_PROPS} stub-runner-boot-eureka-example.jar哪里${SYSTEM_PROPS}包含以下属性列表:spring-doc.cadn.net.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

这样,部署的应用程序可以通过服务向已启动的 WireMock 服务器发送请求 发现。最有可能的是,默认情况下可以设置点 1 到 3application.yml,因为它们不是 可能会改变。这样,您就可以在启动时仅提供要下载的存根列表 短滑道靴子。spring-doc.cadn.net.cn

5.7. 消费者驱动的合同:每个消费者的存根

在某些情况下,同一终结点的两个使用者希望有两个不同的响应。spring-doc.cadn.net.cn

这种方法还可以让您立即知道哪个使用者使用了 API 的哪一部分。您可以删除 API 生成的部分响应,并查看自动生成的测试 失败。 如果没有失败,您可以安全地删除响应的该部分,因为没有人使用它。

考虑以下为生产者定义的名为producer, 它有两个消费者 (foo-consumerbar-consumer):spring-doc.cadn.net.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"
    }
}

您不能为同一个请求生成两个不同的响应。这就是为什么您可以正确打包contracts,然后从stubsPerConsumer特征。spring-doc.cadn.net.cn

在生产者端,消费者可以拥有一个文件夹,其中包含仅与他们相关的合同。通过将stubrunner.stubs-per-consumerflag 到true,我们不再注册所有存根,而只注册那些对应于消费者应用程序的名称。换句话说,我们扫描每个存根的路径,然后,如果它包含一个路径中包含消费者名称的子文件夹,则只有注册它。spring-doc.cadn.net.cn

foo生产者方合同将如下所示spring-doc.cadn.net.cn

.
└── contracts
    ├── bar-consumer
    │   ├── bookReturnedForBar.groovy
    │   └── shouldCallBar.groovy
    └── foo-consumer
        ├── bookReturnedForFoo.groovy
        └── shouldCallFoo.groovy

bar-consumer消费者可以将spring.application.namestubrunner.consumer-namebar-consumer或者,您可以按如下方式设置测试:spring-doc.cadn.net.cn

@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
@SpringBootTest(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)
class StubRunnerStubsPerConsumerSpec extends Specification {
...
}

然后,只有在包含以下内容的路径下注册的存根bar-consumer在其名称中(即来自src/test/resources/contracts/bar-consumer/some/contracts/…​文件夹)被允许引用。spring-doc.cadn.net.cn

您还可以显式设置消费者名称,如下所示:spring-doc.cadn.net.cn

@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
@SpringBootTest
@AutoConfigureStubRunner(ids = "org.springframework.cloud.contract.verifier.stubs:producerWithMultipleConsumers",
        repositoryRoot = "classpath:m2repo/repository/",
        consumerName = "foo-consumer",
        stubsMode = StubRunnerProperties.StubsMode.REMOTE,
        stubsPerConsumer = true)
class StubRunnerStubsPerConsumerWithConsumerNameSpec extends Specification {
...
}

然后,只有在包含foo-consumer在其名称中(即来自src/test/resources/contracts/foo-consumer/some/contracts/…​文件夹)被允许引用。spring-doc.cadn.net.cn

有关更多信息,请参阅第 224 期有关此更改背后原因的信息。spring-doc.cadn.net.cn

5.8. 从某个位置获取存根或合约定义

与其从Artifactory / Nexus 或 Git 中选择存根或合约定义,只需指向驱动器或类路径上的位置。这在多模块项目中特别有用,其中一个模块想要重用另一个模块中的存根或合约,而无需需要在本地 Maven 中实际安装它们存储库将这些更改提交到 Git。spring-doc.cadn.net.cn

为了实现这一点,使用stubs://协议,当存储库根参数设置为在存根运行器或 Spring Cloud Contract 插件中。spring-doc.cadn.net.cn

在此示例中,producer项目已成功构建并在target/stubs文件夹。作为消费者,可以使用stubs://协议。spring-doc.cadn.net.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);

合约和存根可以存储在某个位置,每个生产者都有自己的专用文件夹来存储合约和存根映射。在该文件夹下,每个使用者都可以有自己的设置。要使 Stub Runner 从提供的 ID 中找到专用文件夹,可以传递一个属性stubs.find-producer=true或系统属性stubrunner.stubs.find-producer=true.spring-doc.cadn.net.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 使用者的组 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.cadn.net.cn

作为生产者,当定义契约时,您需要使生成的测试通过才能发布存根。在某些情况下,您希望取消阻止消费者,以便他们可以在测试实际通过之前获取存根。在这种情况下,您应该将此类合约设置为正在进行中。您可以在 Contracts in Progress 部分下阅读更多相关信息。这样,您的测试将不会生成,但存根会生成。spring-doc.cadn.net.cn

作为消费者,您可以切换开关以在运行时生成存根。存根运行器将忽略所有现有的存根映射,并为所有合约定义生成新的存根映射。另一种选择是将stubrunner.generate-stubs系统属性。您可以在下面找到此类设置的示例。spring-doc.cadn.net.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. 无存根失败

默认情况下,如果未找到存根,则存根运行器将失败。要更改该行为,只需设置为falsefailOnNoStubs属性或调用withFailOnNoStubs(false)方法。spring-doc.cadn.net.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.cadn.net.cn

5.11.1. JUnit 和 Spring 的通用属性

您可以使用系统属性或 Spring 配置来设置重复属性 性能。 下表显示了它们的名称及其默认值:spring-doc.cadn.net.cn

属性名称 默认值 描述

存根运行器.min端口spring-doc.cadn.net.cn

10000spring-doc.cadn.net.cn

已启动的带有存根的 WireMock 的端口的最小值。spring-doc.cadn.net.cn

存根运行器.maxPortspring-doc.cadn.net.cn

15000spring-doc.cadn.net.cn

已启动的带有存根的 WireMock 的端口的最大值。spring-doc.cadn.net.cn

stubrunner.repositoryRootspring-doc.cadn.net.cn

Maven 存储库 URL。如果为空,则调用本地 Maven 存储库。spring-doc.cadn.net.cn

存根运行器.分类器spring-doc.cadn.net.cn

存根spring-doc.cadn.net.cn

存根工件的默认分类器。spring-doc.cadn.net.cn

存根运行器.存根模式spring-doc.cadn.net.cn

类路径spring-doc.cadn.net.cn

您想要获取和注册存根的方式spring-doc.cadn.net.cn

存根运行器.idsspring-doc.cadn.net.cn

要下载的常春藤符号存根数组。spring-doc.cadn.net.cn

存根运行器.用户名spring-doc.cadn.net.cn

可选用户名,用于访问存储 JAR 的工具 存根。spring-doc.cadn.net.cn

存根跑器.密码spring-doc.cadn.net.cn

用于访问存储 JAR 的工具的可选密码 存根。spring-doc.cadn.net.cn

stubrunner.stubsPerConsumerspring-doc.cadn.net.cn

falsespring-doc.cadn.net.cn

设置为true如果要使用不同的存根 每个消费者,而不是为每个消费者注册所有存根。spring-doc.cadn.net.cn

stubrunner.consumerNamespring-doc.cadn.net.cn

如果要为每个使用者使用存根,并且想要 覆盖使用者名称,更改此值。spring-doc.cadn.net.cn

5.11.2. 存根运行器存根 ID

您可以在stubrunner.ids系统属性。 他们 使用以下模式:spring-doc.cadn.net.cn

groupId:artifactId:version:classifier:port

请注意version,classifierport是可选的。spring-doc.cadn.net.cn

  • 如果您不提供port,随机选择一个。spring-doc.cadn.net.cn

  • 如果您不提供classifier,则使用默认值。(请注意,您可以 通过以下方式传递一个空分类器:groupId:artifactId:version:).spring-doc.cadn.net.cn

  • 如果您不提供version,然后通过,最新的是 下载。+spring-doc.cadn.net.cn

port表示 WireMock 服务器的端口。spring-doc.cadn.net.cn

从 1.0.4 版开始,您可以提供一系列版本 希望存根运行器考虑。您可以阅读有关以太版本控制的更多信息 范围在这里

6. Spring Cloud 合约 WireMock

Spring Cloud Contract WireMock 模块允许您在 Spring Boot 应用程序。查看示例以了解更多详细信息。spring-doc.cadn.net.cn

如果你有一个使用 Tomcat 作为嵌入式服务器的 Spring Boot 应用程序(即 默认为spring-boot-starter-web),您可以添加spring-cloud-starter-contract-stub-runner到您的类路径并将@AutoConfigureWireMock在测试中使用 Wiremock。Wiremock 作为存根服务器运行,而您 可以使用 Java API 或使用静态 JSON 声明作为 你的测试。以下代码显示了一个示例:spring-doc.cadn.net.cn

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
public class WiremockForDocsTests {

    // A service that calls out over HTTP
    @Autowired
    private Service service;

    @Before
    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!");
    }

}

要在其他端口上启动存根服务器,请使用(例如)@AutoConfigureWireMock(port=9999).对于随机端口,请使用0.存根 服务器端口可以在测试应用程序上下文中使用“wiremock.server.port”绑定 财产。用@AutoConfigureWireMock添加类型WiremockConfiguration自 测试应用程序上下文,它在方法和类之间缓存 具有相同的上下文。Spring 集成测试也是如此。此外,您还可以 注入类型为WireMockServer进入您的测试。 注册的 WireMock 服务器在每个测试类后重置,但是,如果您需要在每个测试方法之后重置它,只需将wiremock.reset-mappings-after-each-test属性设置为true.spring-doc.cadn.net.cn

6.1. 自动注册存根

如果您使用@AutoConfigureWireMock,它从文件中注册 WireMock JSON 存根 系统或类路径(默认情况下,从file:src/test/resources/mappings).您可以 使用stubs属性,可以是 Ant 样式的资源模式或目录。对于目录,*/.json是 附加。以下代码显示了一个示例:spring-doc.cadn.net.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 总是从src/test/resources/mappings 如 以及stubs属性。要更改此行为,您可以 此外,请指定文件根目录,如本文档的下一节所述。
此外,在stubslocation 不被视为 Wiremock 的“默认映射”的一部分,调用 自com.github.tomakehurst.wiremock.client.WireMock.resetToDefaultMappings在测试期间不会导致映射 在stubs包括位置。但是,org.springframework.cloud.contract.wiremock.WireMockTestExecutionListener在每个测试类之后重置映射(包括从存根位置添加映射),并且(可选) 在每个测试方法(由wiremock.reset-mappings-after-each-test属性)。

如果您使用 Spring Cloud Contract 的默认存根 jar,则您的 存根存储在/META-INF/group-id/artifact-id/versions/mappings/文件夹。 如果要从该位置注册所有嵌入 JAR 的所有存根,您可以使用 以下语法:spring-doc.cadn.net.cn

@AutoConfigureWireMock(port = 0, stubs = "classpath*:/META-INF/**/mappings/**/*.json")

6.2. 使用文件指定存根主体

WireMock 可以从类路径或文件系统上的文件中读取响应正文。 在 文件系统的情况下,您可以在 JSON DSL 中看到响应具有bodyFileName而不是(文字)body. 这些文件是相对于根目录解析的(默认情况下,src/test/resources/__files). 要自定义此位置,您可以将files属性中的@AutoConfigureWireMock注释到父级目录的位置(换句话说,__files是一个子目录)。您可以使用 Spring 资源表示法来引用file:…​classpath:…​地点。通用 URL 不是 支持。可以给出一个值列表——在这种情况下,WireMock 解析第一个文件 当它需要找到响应机构时,它就存在。spring-doc.cadn.net.cn

当您配置filesroot,它还会影响 自动加载存根,因为它们来自根位置 在名为mappings.的值files没有 对从stubs属性。

6.3. 替代方案:使用 JUnit 规则

对于更传统的 WireMock 体验,您可以使用 JUnit@Rules启动和停止 服务器。为此,请使用WireMockSpringconvenience 类来获取Options实例,如以下示例所示:spring-doc.cadn.net.cn

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class WiremockForDocsClassRuleTests {

    // Start WireMock on some dynamic port
    // for some reason `dynamicPort()` is not working properly
    @ClassRule
    public static WireMockClassRule wiremock = new WireMockClassRule(
            WireMockSpring.options().dynamicPort());

    // A service that calls out over HTTP to wiremock's port
    @Autowired
    private Service service;

    @Before
    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!");
    }

}

@ClassRule表示服务器在此类中的所有方法之后关闭 已经运行了。spring-doc.cadn.net.cn

6.4. Rest 模板的宽松 SSL 验证

WireMock 允许您使用httpsURL 协议。如果您的 应用程序想要在集成测试中联系该存根服务器,它会发现 SSL 证书无效(自安装证书的常见问题)。 最好的选择通常是重新配置客户端以使用http.如果那不是 选项,您可以要求 Spring 配置一个忽略 SSL 验证错误的 HTTP 客户端 (当然,仅针对测试)。spring-doc.cadn.net.cn

为了以最小的大惊小怪做到这一点,您需要使用 Spring BootRestTemplateBuilder如以下示例所示:spring-doc.cadn.net.cn

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
    return builder.build();
}

你需要RestTemplateBuilder因为构建器是通过回调传递给 初始化它,以便此时可以在客户端中设置 SSL 验证。这 如果您在测试中使用@AutoConfigureWireMock注释或存根运行器。如果您使用 JUnit@Rule方法,您需要添加@AutoConfigureHttpClient注释,如以下示例所示:spring-doc.cadn.net.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));
...
}

如果您使用spring-boot-starter-test,则 Apache HTTP 客户端的 类路径,并且它是由RestTemplateBuilder并配置为忽略 SSL 错误。如果使用默认的java.netclient,则不需要注释(但 没有坏处)。目前不支持其他客户端,但可能会添加 在未来的版本中。spring-doc.cadn.net.cn

禁用自定义RestTemplateBuilder,将wiremock.rest-template-ssl-enabled属性设置为false.spring-doc.cadn.net.cn

6.5. WireMock 和 Spring MVC 模拟

Spring Cloud Contract 提供了一个便利类,可以将 JSON WireMock 存根加载到一个 SpringMockRestServiceServer. 以下代码显示了一个示例:spring-doc.cadn.net.cn

@RunWith(SpringRunner.class)
@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();
    }

}

baseUrlvalue 被附加到所有模拟调用的前面,并且stubs()方法采用存根path 资源模式作为参数。在前面的示例中,在/stubs/resource.json加载到模拟服务器中。如果RestTemplate被要求 访问example.org/,它会获取在该 URL 上声明的响应。 更多 可以指定一个以上的存根模式,并且每个存根模式都可以是一个目录(对于递归所有.json)、固定文件名(如前面的示例所示)或 Ant 样式 模式。JSON 格式是普通的 WireMock 格式,您可以在 WireMock 网站上阅读有关该格式的信息。spring-doc.cadn.net.cn

目前,Spring Cloud Contract Verifier 支持 Tomcat、Jetty 和 Undertow 作为 Spring Boot 嵌入式服务器,Wiremock 本身对特定的 Jetty 版本(当前为 9.2)。要使用原生 Jetty,您需要添加原生 Wiremock 依赖项并排除 Spring Boot 容器(如果有)。spring-doc.cadn.net.cn

7. 构建工具集成

您可以通过多种方式运行测试生成和存根执行。最常见的是 如下:spring-doc.cadn.net.cn

8. 接下来要读什么

如果您想了解有关本节中讨论的任何类的更多信息,可以直接浏览源代码。如果您有具体问题,请参阅作方法部分。spring-doc.cadn.net.cn

如果您对 Spring Cloud Contract 的核心功能感到满意,您可以继续阅读并阅读关于 Spring Cloud Contract 的高级功能spring-doc.cadn.net.cn