85. Spring Cloud Contract Stub Runner

使用 Spring Cloud Contract Verifier 时可能遇到的问题之一是将生成的 WireMock JSON 存根从服务器端传递到 client 端(或传递给各种 clients)。对于消息传递,client-side 生成也是如此。

复制 JSON files 并手动设置 client 端以进行消息传递是不可能的。这就是我们引入 Spring Cloud Contract Stub Runner 的原因。它可以自动为您下载和运行存根。

85.1 快照版本

将附加快照 repository 添加到build.gradle文件以使用快照版本,这些版本会在每次成功 build 后自动上载:

Maven 的.

<repositories>
	<repository>
		<id>spring-snapshots</id>
		<name>Spring Snapshots</name>
		<url>https://repo.spring.io/snapshot</url>
		<snapshots>
			<enabled>true</enabled>
		</snapshots>
	</repository>
	<repository>
		<id>spring-milestones</id>
		<name>Spring Milestones</name>
		<url>https://repo.spring.io/milestone</url>
		<snapshots>
			<enabled>false</enabled>
		</snapshots>
	</repository>
	<repository>
		<id>spring-releases</id>
		<name>Spring Releases</name>
		<url>https://repo.spring.io/release</url>
		<snapshots>
			<enabled>false</enabled>
		</snapshots>
	</repository>
</repositories>
<pluginRepositories>
	<pluginRepository>
		<id>spring-snapshots</id>
		<name>Spring Snapshots</name>
		<url>https://repo.spring.io/snapshot</url>
		<snapshots>
			<enabled>true</enabled>
		</snapshots>
	</pluginRepository>
	<pluginRepository>
		<id>spring-milestones</id>
		<name>Spring Milestones</name>
		<url>https://repo.spring.io/milestone</url>
		<snapshots>
			<enabled>false</enabled>
		</snapshots>
	</pluginRepository>
	<pluginRepository>
		<id>spring-releases</id>
		<name>Spring Releases</name>
		<url>https://repo.spring.io/release</url>
		<snapshots>
			<enabled>false</enabled>
		</snapshots>
	</pluginRepository>
</pluginRepositories>

摇篮.

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

85.2 将 Stubs 发布为 JARs

最简单的方法是集中存根的方式。对于 example,您可以将它们保存为 Maven repository 中的 jars。

对于 Maven 和 Gradle,设置准备就绪。但是,您可以根据需要自定义它。

Maven 的.

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

<!-- Next add the assembly plugin to your build -->
<!-- we want the assembly plugin to generate the JAR -->
<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-assembly-plugin</artifactId>
	<executions>
		<execution>
			<id>stub</id>
			<phase>prepare-package</phase>
			<goals>
				<goal>single</goal>
			</goals>
			<inherited>false</inherited>
			<configuration>
				<attach>true</attach>
				<descriptors>
					$../../../../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 http://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>$../../../../src/test/resources/contracts</directory>
			<outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/contracts</outputDirectory>
			<includes>
				<include>**/*.groovy</include>
			</includes>
		</fileSet>
	</fileSets>
</assembly>

摇篮.

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

85.3 Stub Runner Core

运行服务协作者的存根。将存根视为 contracts of services 允许使用 stub-runner 作为Consumer Driven Contracts的 implementation。

Stub Runner 允许您自动下载所提供的依赖项的存根(或从 classpath 中选择那些存根),为它们启动 WireMock 服务器并使用适当的存根定义提供它们。对于消息传递,定义了特殊的存根 routes。

85.3.1 检索存根

您可以选择以下获取存根的选项

  • 基于以太的解决方案,使用 Artifactory/Nexus 的存根下载 JAR

  • Classpath 扫描解决方案,通过 pattern 搜索 classpath 以检索存根

  • 编写自己org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder的 implementation 以进行完全自定义

后一个 example 在自定义 Stub Runner部分中描述。

Stub 下载

如果你提供的stubrunner.repositoryRootstubrunner.workOffline flag 将被设置为true,那么 Stub Runner 将连接到给定的服务器并下载所需的 jars。然后它将 JAR 解压缩到一个临时文件夹,并在进一步的 contract 处理中引用那些 files。

例:

@AutoConfigureStubRunner(repositoryRoot="http://foo.bar", ids = "com.example:beer-api-producer:+:stubs:8095")

Classpath 扫描

如果你****不提供stubrunner.repositoryRootstubrunner.workOffline flag 将设置为false(这是默认值),那么 classpath 将被扫描。我们来看下面的例子:

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

如果已将依赖项添加到 classpath

Maven 的.

<dependency>
    <groupId>com.example</groupId>
    <artifactId>beer-api-producer-restdocs</artifactId>
    <classifier>stubs</classifier>
    <version>0.0.1-SNAPSHOT</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>*</groupId>
            <artifactId>*</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.example.foo</groupId>
    <artifactId>bar</artifactId>
    <classifier>superstubs</classifier>
    <version>1.0.0</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>*</groupId>
            <artifactId>*</artifactId>
        </exclusion>
    </exclusions>
</dependency>

摇篮.

testCompile("com.example:beer-api-producer-restdocs:0.0.1-SNAPSHOT:stubs") {
    transitive = false
}
testCompile("com.example.foo:bar:1.0.0:superstubs") {
    transitive = false
}

然后将扫描 classpath 上的以下位置。对于com.example:beer-api-producer-restdocs

  • /META-INF/com.example/beer-api-producer-restdocs/ /.

  • /contracts/com.example/beer-api-producer-restdocs/ /.

  • /mappings/com.example/beer-api-producer-restdocs/ /.

com.example.foo:bar

  • /META-INF/com.example.foo/bar/ /.

  • /contracts/com.example.foo/bar/ /.

  • /mappings/com.example.foo/bar/ /.

如您所见,在打包 producer 存根时必须显式提供 group 和 artifact ID。

producer 会像这样设置 contracts:

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

实现适当的短截包装。

或者使用Maven 程序集插件Gradle Jar任务,您必须在存根 jar 中创建以下结构。

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

通过维护此结构,可以扫描 classpath,您可以从消息传递/ HTTP 存根中获益,而无需下载 artifacts。

85.3.2 运行存根

使用主应用程序运行

您可以将以下选项设置为主 class:

-c, --classifier                Suffix for the jar containing stubs (e.
                                  g. 'stubs' if the stub jar would
                                  have a 'stubs' classifier for stubs:
                                  foobar-stubs ). Defaults to 'stubs'
                                  (default: stubs)
--maxPort, --maxp <Integer>     Maximum port value to be assigned to
                                  the WireMock instance. Defaults to
                                  15000 (default: 15000)
--minPort, --minp <Integer>     Minimum port value to be assigned to
                                  the WireMock instance. Defaults to
                                  10000 (default: 10000)
-p, --password                  Password to user when connecting to
                                  repository
--phost, --proxyHost            Proxy host to use for repository
                                  requests
--pport, --proxyPort [Integer]  Proxy port to use for repository
                                  requests
-r, --root                      Location of a Jar containing server
                                  where you keep your stubs (e.g. http:
                                  //nexus.
                                  net/content/repositories/repository)
-s, --stubs                     Comma separated list of Ivy
                                  representation of jars with stubs.
                                  Eg. groupid:artifactid1,groupid2:
                                  artifactid2:classifier
-u, --username                  Username to user when connecting to
                                  repository
--wo, --workOffline             Switch to work offline. Defaults to
                                  'false'

HTTP Stubs

存根在 JSON 文档中定义,其语法在WireMock 文档中定义

例:

{
    "request": {
        "method": "GET",
        "url": "/ping"
    },
    "response": {
        "status": 200,
        "body": "pong",
        "headers": {
            "Content-Type": "text/plain"
        }
    }
}

查看已注册的映射

每个存根协作者都会在__/admin/端点下公开已定义映射的列表。

您还可以使用mappingsOutputFolder property 将映射转储到 files。对于基于注释的方法,它看起来像这样

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

对于像这样的 JUnit 方法:

@ClassRule @Shared StubRunnerRule rule = new StubRunnerRule()
			.repoRoot("http://some_url")
			.downloadStub("a.b.c", "loanIssuance")
			.downloadStub("a.b.c:fraudDetectionServer")
			.withMappingsOutputFolder("target/outputmappings")

然后,如果你签出文件夹target/outputmappings,你会看到以下结构

.
├── fraudDetectionServer_13705
└── loanIssuance_12255

这意味着注册了两个存根。 fraudDetectionServer在 port 13705loanIssuance注册 port 12255。如果我们看一下 files 中的一个,我们会看到(对于 WireMock)给定服务器可用的映射:

[{
  "id" : "f9152eb9-bf77-4c38-8289-90be7d10d0d7",
  "request" : {
    "url" : "/name",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "body" : "fraudDetectionServer"
  },
  "uuid" : "f9152eb9-bf77-4c38-8289-90be7d10d0d7"
},
...
]

Messaging Stubs

根据提供的 Stub Runner 依赖关系和 DSL,将自动设置消息传递 routes。

85.4 Stub Runner JUnit Rule

Stub Runner 附带了一个 JUnit 规则,因此您可以非常轻松地下载和运行给定 group 和 artifact id 的存根:

@ClassRule public static StubRunnerRule rule = new StubRunnerRule()
		.repoRoot(repoRoot())
		.downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")
		.downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer");

执行该规则后,Stub Runner 将连接到您的 Maven repository,并且对于给定的依赖项列表,尝试:

  • 下载它们

  • 在本地缓存它们

  • 将它们解压缩到一个临时文件夹

  • 从提供的 ports/provided port 范围内的 random port 上为每个 Maven 依赖项启动 WireMock 服务器

  • 使用有效 WireMock 定义的所有 JSON files 为 WireMock 服务器提供信息

  • 也可以发送消息(记得传递MessageVerifier接口的 implementation)

Stub Runner 使用Eclipse 以太机制来下载 Maven 依赖项。检查他们的docs以获取更多信息。

由于StubRunnerRule实现StubFinder,它允许您查找已启动的存根:

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;

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
	 * @return URL of a running stub or throws exception if not found
	 */
	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
	 */
	URL findStubUrl(String ivyNotation) throws StubNotFoundException;

	/**
	 * Returns all running stubs
	 */
	RunningStubs findAllRunningStubs();

	/**
	 * Returns the list of Contracts
	 */
	Map<StubConfiguration, Collection<Contract>> getContracts();
}

_ Spock 测试中的使用示例:

@ClassRule @Shared StubRunnerRule rule = new StubRunnerRule()
		.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()
}

_JUnit 测试中的用法示例:

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

检查Common properties for JUnit 和 Spring,了解有关如何应用 Stub Runner 的 global configuration 的更多信息。

要将 JUnit 规则与消息传递一起使用,您必须为规则构建器(e.g. rule.messageVerifier(new MyMessageVerifier()))提供MessageVerifier接口的 implementation。如果您不这样做,那么每当您尝试发送消息时,都会抛出 exception。

85.4.1 Maven 设置

存根下载程序为不同的本地 repository 文件夹提供 Maven 设置。目前不考虑 repositories 和 profiles 的身份验证详细信息,因此您需要使用上面提到的 properties 指定它。

85.4.2 提供固定端口

您也可以在固定端口上运行存根。你可以用两种不同的方式做到这一点。一种是在 properties 中传递它,另一种是通过 JUnit 规则的 fluent API 传递它。

85.4.3 Fluent API

使用StubRunnerRule时,您可以添加要下载的存根,然后将 port 传递给上次下载的存根。

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

您可以看到,对于此 example,以下测试有效:

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

85.4.4 Stub Runner with Spring

_Set up Spring 配置 Stub Runner 项目。

通过在 configuration 文件中提供存根列表,Stub Runner 会自动下载并在 WireMock 中注册选定的存根。

如果要查找存根依赖关系的 URL,可以自动装配StubFinder接口并使用其方法,如下所示:

@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
@SpringBootTest(properties = [" stubrunner.cloud.enabled=false",
		"stubrunner.camel.enabled=false",
		'foo=${stubrunner.runningstubs.fraudDetectionServer.port}'])
@AutoConfigureStubRunner(mappingsOutputFolder = "target/outputmappings/")
@ActiveProfiles("test")
class StubRunnerConfigurationSpec extends Specification {

	@Autowired StubFinder stubFinder
	@Autowired Environment environment
	@Value('${foo}') Integer foo

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

	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'
	}

	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)
	}

	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
			foo == 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 {}
}

对于以下 configuration 文件:

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

您也可以使用@AutoConfigureStubRunner中的 properties 而不是 properties。您可以在下面找到通过在 annotation 上设置值来实现相同结果的示例。

@AutoConfigureStubRunner(
		ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance",
		"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer",
		"org.springframework.cloud.contract.verifier.stubs:bootService"],
		repositoryRoot = "classpath:m2repo/repository/")

Stub Runner Spring 为每个注册的 WireMock 服务器按以下方式注册环境变量。 例如 Stub Runner ID com.example:foocom.example:bar

  • stubrunner.runningstubs.foo.port

  • stubrunner.runningstubs.bar.port

您可以在 code 中 reference。

85.5 Stub Runner Spring Cloud

Stub Runner 可以与 Spring Cloud 集成。

对于现实生活中的例子,您可以查看

85.5.1 Stubbing Service Discovery

Stub Runner Spring Cloud最重要的特征是它的存在

  • DiscoveryClient

  • Ribbon ServerList

这意味着无论你使用的是 Zookeeper,Consul,Eureka 还是其他任何东西,你都不需要在测试中使用它。我们正在启动依赖项的 WireMock 实例,并且只要您使用Feign,直接负载均衡RestTemplateDiscoveryClient来调用那些存根服务器而不是调用真正的服务发现工具,我们就会告诉您的 application。

对于 example,此测试将通过

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'
}

对于以下 configuration 文件

stubrunner:
  idsToServiceIds:
    ivyNotation: someValueInsideYourCode
    fraudDetectionServer: someNameThatShouldMapFraudDetectionServer

测试 profiles 和服务发现

在 integration 测试中,您通常不希望既不调用发现服务(e.g .Eureka)也不想调用 Config Server。这就是为什么你要创建一个额外的测试配置,你要在其中禁用这些 features。

由于spring-cloud-commons的某些限制来实现这一点,你可以通过如下所示的静态块禁用这些 properties(_Eample 为 Eureka)

//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");
    }

85.5.2 其他 Configuration

您可以使用stubrunner.idsToServiceIds: map 与应用的 name 匹配存根的 artifactId。您可以通过提供以下内容来禁用 Stub Runner Ribbon 支持:stubrunner.cloud.ribbon.enabled等于false您可以通过提供以下内容来禁用 Stub Runner 支持:stubrunner.cloud.enabled等于false

默认情况下,所有服务发现都将被存根。这意味着,无论您是否拥有现有的DiscoveryClient,其结果都将被忽略。但是,如果要重用它,只需将stubrunner.cloud.delegate.enabled设置为true,然后将现有的DiscoveryClient结果与存根的结果合并。

Stub Runner 使用的默认 Maven configuration 可以通过以下系统 properties 或环境变量进行调整

  • maven.repo.local - 自定义 maven local repository 位置的路径

  • org.apache.maven.user-settings - 自定义 maven 用户设置位置的路径

  • org.apache.maven.global-settings - maven global 设置位置的路径

85.6 Stub Runner Boot Application

Spring Cloud Contract Stub Runner Boot 是一个 Spring Boot application,它公开 REST endpoints 以触发消息标签并访问启动的 WireMock 服务器。

其中一个 use-cases 是在部署的 application 上运行一些冒烟(端对端)测试。您可以查看Spring Cloud 管道项目以获取更多信息。

85.6.1 怎么用?

Stub Runner Server

只需添加

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

使用@EnableStubRunnerServer注释 class,build fat-jar 并准备好了!

对于 properties,请检查Stub Runner Spring部分。

Stub Runner Server Fat Jar

您可以从 Maven 下载独立的 JAR(对于 example,对于 version 1.2.3.RELEASE),如下所示:

$ wget -O stub-runner.jar 'https://search.maven.org/remote_content?g=org.springframework.cloud&a=spring-cloud-contract-stub-runner-boot&v=1.2.3.RELEASE'
$ java -jar stub-runner.jar --stubrunner.ids=... --stubrunner.repositoryRoot=...

Spring Cloud CLI

Spring Cloud CLI项目的1.4.0.RELEASE version 开始,您可以通过执行spring cloud stubrunner来启动 Stub Runner Boot。

在 order 中传递 configuration 只需在当前工作目录或名为config~/.spring-cloud的子目录中创建一个stubrunner.yml文件。该文件可能如下所示(本地安装的 running 存根的 example)

stubrunner.yml.

stubrunner:
  workOffline: true
  ids:
    - com.example:beer-api-producer:+:9876

然后只需从终端窗口调用spring cloud stubrunner即可启动 Stub Runner 服务器。它将在 port 8750上提供。

85.6.2 Endpoints

HTTP

  • GET /stubs - 以ivy:integer表示法返回所有运行存根的列表

  • GET /stubs/{ivy} - 返回给定ivy表示法的 port(当调用端点ivy时也可以只artifactId)

消息

对于消息传递

  • GET /triggers - 以ivy : [ label1, label2 …]表示法返回所有 running 标签的列表

  • POST /triggers/{label} - 用label执行触发器

  • POST /triggers/{ivy}/{label} - 为给定的ivy表示法执行label的触发器(当调用端点ivy时也可以只artifactId)

85.6.3 示例

@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
			response.body.as(Integer) > 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=")
	}

}

85.6.4 Stub Runner Boot with Service Discovery

使用 Stub Runner Boot 的一种可能性是将其用作“smoke-tests”的存根的源。这是什么意思?假设您不希望在 order 中将 50 个微服务部署到测试环境,以检查您的 application 是否正常工作。您已经在 build process 期间执行了一系列测试,但您还希望确保 application 的包装正常。你可以做的是将你的应用程序部署到一个环境,启动它并运行几个测试,看看它是否正常工作。我们可以将这些测试称为 smoke-tests,因为他们的 idea 只是检查一些测试场景。

这种方法的问题在于,如果您正在使用微服务,那么您很可能正在使用服务发现工具。 Stub Runner Boot 允许您通过启动所需的存根并在服务发现工具中注册它们来解决此问题。让我们看一下使用 Eureka 的这种设置的示例。让我们假设 Eureka 已经__unning 了。

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

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

}

如您所见,我们想要启动 Stub Runner Boot 服务器@EnableStubRunnerServer,启用 Eureka client @EnableEurekaClient,我们希望启用存根运行 feature @AutoConfigureStubRunner

现在让我们假设我们想要启动这个 application,以便自动注册存根。我们可以通过 running app java -jar ${SYSTEM_PROPS} stub-runner-boot-eureka-example.jar来实现,其中${SYSTEM_PROPS}将包含以下 properties 列表

-Dstubrunner.repositoryRoot=http://repo.spring.io/snapshots (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 artifactId to serviceId mapping

这样,您部署的 application 可以通过服务发现向已启动的 WireMock 服务器发送请求。最有可能的点 1-3 可以在application.yml中默认设置,因为它们不太可能改变。这样,只要启动 Stub Runner Boot,就只能提供要下载的存根列表。

85.7 Stubs Per Consumer

存在两个相同端点的消费者想要具有 2 个不同响应的情况。

此方法还允许您立即知道哪个 consumer 正在使用您的 API 的哪个部分。您可以删除 API 生成的部分响应,并且可以看到哪些自动生成的测试失败。如果 none 失败,那么您可以安全地删除响应的那部分,因为没有人使用它。

让我们看一下为 producer 定义的 contract 的以下 example,名为producer。有 2 个消费者:foo-consumerbar-consumer

消费者 foo-service

request {
   url '/foo'
   method GET()
}
response {
    status 200
    body(
       foo: "foo"
    }
}

消费者 bar-service

request {
   url '/foo'
   method GET()
}
response {
    status 200
    body(
       bar: "bar"
    }
}

您不能为同一请求生成 2 个不同的响应。这就是为什么你可以正确打包 contracts 然后从stubsPerConsumer feature 中获利。

在 producer 端,消费者可以拥有一个包含仅与它们相关的 contracts 的文件夹。通过将stubrunner.stubs-per-consumer flag 设置为true,我们不再注册所有存根,而只注册与 consumer application 的 name 对应的存根。换句话说,我们将扫描每个存根的路径,如果它包含路径中只有 consumer 的 name 的子文件夹,那么它是否会被注册。

foo producer 方面,contracts 看起来像这样

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

作为bar-consumer consumer,您可以将spring.application.namestubrunner.consumer-name设置为bar-consumer或者按如下方式设置测试:

@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/",
		stubsPerConsumer = true)
class StubRunnerStubsPerConsumerSpec extends Specification {
...
}

然后,只允许引用在 name 中包含bar-consumer的路径下注册的存根(i.e.来自src/test/resources/contracts/bar-consumer/some/contracts/…文件夹的那些)。

或者明确设置 consumer name

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

然后,只允许引用在 name 中包含foo-consumer的路径下注册的存根(i.e.来自src/test/resources/contracts/foo-consumer/some/contracts/…文件夹的那些)。

您可以查看问题 224以获取有关此更改背后原因的更多信息。

85.8 Common

本节简要介绍 common properties,包括:

85.8.1 Common Properties for JUnit 和 Spring

您可以使用 system properties 或 Spring configuration properties 设置重复的 properties。以下是他们的名字及其默认值:

Property name默认值描述
stubrunner.minPort10000带有存根的已启动 WireMock 的 port 的最小 value。
stubrunner.maxPort15000带有存根的已启动 WireMock 的 port 的最大 value。
stubrunner.repositoryRoot Maven repo URL。如果为空,则调用本地 maven repo。
stubrunner.classifier存根stub artifacts 的默认分类器。
stubrunner.workOffline如果 true,则不要联系任何 remote repositories 来下载存根。
stubrunner.ids _Avy 的 Ivy 符号存根要下载。
stubrunner.username 用于访问 store JAR 与存根的工具的可选用户名。
stubrunner.password 可选密码,用于访问使用存根存储 JAR 的工具。
stubrunner.stubsPerConsumer如果要为每个 consumer 使用不同的存根,而不是为每个 consumer 注册所有存根,则设置为true
stubrunner.consumerName 如果你想为每个 consumer 使用一个存根,并想要覆盖 consumer name,只需改变这个 value。

85.8.2 Stub Runner 存根 ID

您可以通过stubrunner.ids system property 提供要下载的存根。他们遵循这个 pattern:

groupId:artifactId:version:classifier:port

请注意versionclassifierport是可选的。

  • 如果您不提供port,将选择一个随机的。

  • 如果未提供classifier,则使用默认值。 (请注意,您可以通过这种方式传递空分类器:groupId:artifactId:version:)。

  • 如果您未提供version,则将传递+并下载最新的+

port表示 WireMock 服务器的 port。

从 version 1.0.4 开始,您可以提供一系列您希望 Stub Runner 考虑的版本。您可以阅读有关这里的以太版本范围很广的更多信息。

85.9 Stub Runner Docker

我们正在发布一个spring-cloud/spring-cloud-contract-stub-runner Docker 镜像,它将启动 Stub Runner 的独立 version。

如果您想了解更多关于 Maven,artifact ids,group ids,classifiers 和 Artifact Managers 的基础知识,请点击这里第 83.6 节,“Docker 项目”

85.9.1 如何使用它

只需执行泊坞窗图像。您可以将任何Section 85.8.1,“ Common Properties for JUnit and Spring”作为环境变量传递。惯例是所有字母都应该是大写的。驼峰案例符号应该和点(.)应该通过下划线(_)分开。 E.g。 stubrunner.repositoryRoot property 应表示为STUBRUNNER_REPOSITORY_ROOT环境变量。

85.9.2 非 JVM 项目中 client 端用法的示例

我们想使用在第 83.6.4 节,“服务器端(nodejs)” step 中创建的存根。让我们假设我们想在 port 9876上运行存根。 NodeJS code 可在此处获得:

$ git clone https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs
$ cd bookstore

让我们用存根来运行 Stub Runner Boot application。

# Provide the Spring Cloud Contract Docker version
$ SC_CONTRACT_DOCKER_VERSION="..."
# The IP at which the app is running and Docker container can reach it
$ APP_IP="192.168.0.100"
# Spring Cloud Contract Stub Runner properties
$ STUBRUNNER_PORT="8083"
# Stub coordinates 'groupId:artifactId:version:classifier:port'
$ STUBRUNNER_IDS="com.example:bookstore:0.0.1.RELEASE:stubs:9876"
$ STUBRUNNER_REPOSITORY_ROOT="http://${APP_IP}:8081/artifactory/libs-release-local"
# Run the docker with Stub Runner Boot
$ docker run  --rm -e "STUBRUNNER_IDS=${STUBRUNNER_IDS}" -e "STUBRUNNER_REPOSITORY_ROOT=${STUBRUNNER_REPOSITORY_ROOT}" -p "${STUBRUNNER_PORT}:${STUBRUNNER_PORT}" -p "9876:9876" springcloud/spring-cloud-contract-stub-runner:"${SC_CONTRACT_DOCKER_VERSION}"

发生了什么事

  • 一个独立的 Stub Runner application 开始了

  • 它在 port 9876上下载了坐标为com.example:bookstore:0.0.1.RELEASE:stubs的存根

  • 它是从 Artifactory 下载运行http://192.168.0.100:8081/artifactory/libs-release-local

  • 过了一会儿,Stub Runner 将在 port 8083上运行

  • 并且存根将在 port 9876运行

在服务器端,我们构建了一个有状态的存根。让我们使用 curl 断言存根设置正确。

# let's execute the first request (no response is returned)
$ curl -H "Content-Type:application/json" -X POST --data '{ "title" : "Title", "genre" : "Genre", "description" : "Description", "author" : "Author", "publisher" : "Publisher", "pages" : 100, "image_url" : "https://d213dhlpdb53mu.cloudfront.net/assets/pivotal-square-logo-41418bd391196c3022f3cd9f3959b3f6d7764c47873d858583384e759c7db435.svg", "buy_url" : "https://pivotal.io" }' http://localhost:9876/api/books
# Now time for the second request
$ curl -X GET http://localhost:9876/api/books
# You will receive contents of the JSON