使用Spring Boot的消费者驱动合同

18-09-07 banq
                   

在本文中,我们将讨论消费者驱动开发的细节。

#问题

主要问题是基于API接口上的消费者和生产者之间的冲突,当开发任何api时,你应该考虑的是你的客户的舒适度。如果你所做的更改打破了客户端的体验,那完全是一个笑话,本文讨论了消费者和生产者服务之间这种协议挑战。

#解决之道

消费者驱动合同(CDC)是确保生产者和消费者之间、分布式系统或微服务中基于HTTP或基于消息或基于事件的软件之间的执行正确的协议合约或合同。

Spring Cloud Contract是基于JVM的语言的CDC开发的实现,它也支持非基于jvm的语言,它将TDD提升到软件(api)设计和架构的水平,我称之为CDC - > Client Driven Development,因为是客户端(消费者)驱动生产者API的变化。

#为什么?

有关上面提到的一些问题和解决方案,你可能想为什么我们需要这种方法?消费者和生产者之间的握手在微服务架构设计存在一些挑战,因为,生产者所做的改变很难在消费者方面进行测试。见下面这个微服务图片:

当试图测试与许多其他服务进行通信的应用程序时,我们可以在没有消费者驱动合同的情况下做以下事情:

1. 部署所有微服务并执行端到端测试。

2. 在测试中模拟Mock其他服务。

两种方法都有其优点和缺点。

部署所有微服务;

优点 - >模拟生产,测试实际服务,更可靠

缺点 - >长期运行,难以调试,许多成本(部署许多应用程序,许多资源,如数据库,缓存等),存在非常晚的反馈

模拟Mock其他服务;

优点 - >非常快速的反馈,无需设置基础设施

缺点 - >不可靠,你可以对K8s的prod测试和失败检查

为解决这些问题,创建了Spring Cloud Contract。

我在开发应用程序的新功能时遵循这些步骤;

1. 准备合同。

2. 从准备好的合同中生成测试。

3. 采用TDD和红绿色风格编码。

4. 功能完成后,你可以创建存根(合约)jar。

5. 消费者可以使用这个存根罐来集成api。

好吧,让我们按这个步骤编码;

首先,我们需要使用Groovy DSL建立合同,如下所示;

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

Contract.make {
    description "should retrieve account"
    request {
        method GET()
        headers {
            accept(applicationJson())
        }
        url ('/api/v1/accounts'){
            queryParameters {
                parameter("accountId", 1L)
            }
        }
    }
    response {
        status OK()
        headers {
            contentType(applicationJson())
        }
        body(file("retrieveAccountResponse.json"))
    }
}
<p>

这是一个在生产者端检索帐户的合同定义,我们为检索帐户制作了这个样本合同。请求'api/v1/accounts'端点是使用HTTP get方法和查询参数'accountId。

响应中指定Http标头为json,最后我们期望响应状态应该是正常的(200)并且响应头contentType应该是json并且响应体应该等于json响应文件。响应内容如下;

{
    "name": "Name",
    "surname": "Surname",
    "gender": "Gender",
    "gsmNumber": "GsmNumber",
    "identifier": "Identifier",
    "createdDate": 1514851199,
    "updatedDate": 1514851199
}
<p>

在生成测试类之前,我们应该配置合同插件;

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-contract-verifier</artifactId>
            <scope>test</scope>
</dependency>
 <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-contract-maven-plugin</artifactId>
                <extensions>true</extensions>
                <configuration>
                    <!-- Provide the base class for your auto-generated tests -->
                    <baseClassForTests>com.caysever.producer.ProducerBaseContractTest</baseClassForTests>
                    <testMode>EXPLICIT</testMode>
                </configuration>
            </plugin>
        </plugins>
    </build>


<p>

生产者方面的pom.xml

你可以使用TDD样式从合同和编码中生成测试类,这可以通过generateTests maven目标来做到这一点 - > mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:2.0.1.RELEASE:generateTests

生成的测试置于target/generated-test-sources下,生成测试如下代码:

public class ContractVerifierTest extends ProducerBaseContractTest {

	@Test
	public void validate_retrieveAccountContract() throws Exception {
		// given:
			RequestSpecification request = given()
					.header("Accept", "application/json");

		// when:
			Response response = given().spec(request)
					.queryParam("accountId","1")
					.get("/api/v1/accounts");

		// 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("['surname']").isEqualTo("Surname");
			assertThatJson(parsedJson).field("['updatedDate']").isEqualTo(1514851199);
			assertThatJson(parsedJson).field("['gender']").isEqualTo("Gender");
			assertThatJson(parsedJson).field("['name']").isEqualTo("Name");
			assertThatJson(parsedJson).field("['createdDate']").isEqualTo(1514851199);
			assertThatJson(parsedJson).field("['identifier']").isEqualTo("Identifier");
			assertThatJson(parsedJson).field("['gsmNumber']").isEqualTo("GsmNumber");
	}

<p>

你现在可以将其作为正常的junit测试运行,测试通过后,可以与你的客户和消费者分享你的合同。

注意:

当修改端点(如重命名url或添加/删除参数)时,应修改合同,如果你不修改,则构建无法通过。

Spring cloud contract插件为你生成存根stub的jar包,可以将其部署到artifactory或本地参考local repo,Spring云契约支持不同的存根模式,例如classpath或本地m2 repo或远程artifactory(神器?),我们这里将使用本地m2模式。

让我们看看如何消费存根stub:

@ExtendWith(SpringExtension.class)
@AutoConfigureWebTestClient
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureStubRunner(ids = "com.caysever:producer:+:8090", stubsMode = StubRunnerProperties.StubsMode.LOCAL)
class ProducerVerifierTest {

    @Autowired
    private WebTestClient webTestClient;

    @Test
    void should_retrieveAccountById() {
        //given
        Long accountId = 1L;

        //when
        Account account = webTestClient.get()
                .uri("http://localhost:8090/api/v1/accounts?accountId={accountId}", accountId)
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus().isOk()
                .expectBody(Account.class)
                .returnResult()
                .getResponseBody();

        //then
        assertThat(account).isNotNull();
        assertThat(account.getName()).isEqualTo("Name");
        assertThat(account.getSurname()).isEqualTo("Surname");
        assertThat(account.getGender()).isEqualTo("Gender");
        assertThat(account.getGsmNumber()).isEqualTo("GsmNumber");
        assertThat(account.getIdentifier()).isEqualTo("Identifier");
        assertThat(account.getCreatedDate()).isEqualTo(1514851199);
        assertThat(account.getUpdatedDate()).isEqualTo(1514851199);
    }
}
<p>

使用@AutoConfigureStubRunner注释Spring设置wiremock服务器,真正的生产者api应该在8090端口,我们创建REST http请求并断言响应数据。如果任何步骤发生失败,则测试交付的CI / CD管道都不会通过,即使在你的本地环境而不是CI服务器上。

github repo上提供的本案例所有源代码

Consumer Driven Contract with Spring Boot – Alican