spring cloud contract的应用实现与概念理解-服务提供者一侧的落地

发布于:2022-12-28 ⋅ 阅读:(413) ⋅ 点赞:(0)

如题,本文是在前一篇“spring cloud contract的应用实现与概念理解-服务请求者一侧的落地”的基础上,续写服务提供者一侧的有关实现与理解。

通过对官网文章的学习和编程实践的验证,contract的应用实现,实际上并不须要在同一个工程中同时完成stub桩(给服务消费者用)和mock模拟(给服务提供者用)。经常看到的例子中,既包含“spring-cloud-starter-contract-verifier"又包含“spring-cloud-starter-contract-stub-runner”(POM.XML文件中配置依赖),那也许是因为微服务中,很多工程实际上既是另外某个服务消费者,也是其他服务或者客户端的服务提供者。如果把问题分解,单独从服务提供者的角度而言,实际上更简单。

集成测试的背景、意义,contract在集成测试中的位置和作用,在我的前一篇文章spring cloud contract的应用实现与概念理解-服务请求者一侧的落地-细节较多避免踩坑卡壳已经进行了比较感性的描述,本文就不再累述。下面就直接从服务提供者一侧开发的目的、过程和内容来记述。首先,服务提供者利用contract的目的抛开给消费者提供确定的接口规格桩外,对于服务提供者自身,是形成一个测试发起者的模拟(mock),向协议脚本中定义的服务URL(REST API如此,消息队列是另外的参数,不在本文中深究)提交预期定好的请求,服务提供者此时的服务实现应该已经完成开发并能响应,contract将会在测试方法中对响应进行比对验证,所有要素的符合预期的话,则达到了验证服务有效性、正确性的目的。

服务提供者一侧contract的编辑和实现,实际上很简单。具体步骤其实,在前一篇文章中已经提到了。此时,首先由服务消费者和服务提供者设计讨论服务的接口规格,然后由服务提供者在版本中POM.XML增加contract的插件配置,再针对要测试的服务接口编辑contract脚本文件就可以了。就这么简单。过程如下图:后面将会解析其背后的逻辑。

 过程和内容而言,很简单。

首先,在POM中增加(假设第一次在这个工程中开发contract测试)

首先,增加spring cloud的版本定义,因为contract实际上是spring cloud的组件

<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud-release.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

其中,spring cloud 和 contract的版本配置如下(2022年9月 经过实践验证,可用)

<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-cloud-release.version>2021.0.3</spring-cloud-release.version>
        <spring-cloud-contract.version>3.1.3</spring-cloud-contract.version>
</properties>

然后,加入contract构建插件,这个插件的作用在于依据contract脚本生成stub桩。

在build--plugins下增加

<plugin>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-contract-maven-plugin</artifactId>
				<version>${spring-cloud-contract.version}</version>
				<extensions>true</extensions>
				<configuration>
					<!-- <packageWithBaseClasses>bocd.com.cn.contract-sample</packageWithBaseClasses> -->
					<baseClassForTests>toni.com.cn.contract_sample.BaseTestClass</baseClassForTests>
				</configuration>
				<!-- if additional dependencies are needed e.g. for Pact -->
				<dependencies>
					<dependency>
						<groupId>org.springframework.cloud</groupId>
						<artifactId>spring-cloud-contract-pact</artifactId>
						<version>${spring-cloud-contract.version}</version>
					</dependency>
				</dependencies>
			</plugin>

设定此插件的同时,按照官网的说法,服务提供者一侧的有关依赖就也包含在里面了。不过,我在具体案例中,还是加入了verifier依赖,如下:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-contract-verifier</artifactId>
            <scope>test</scope>
</dependency>

值得专门说明的地方,在“spring-cloud-contract-maven-plugin”中,有一行<baseClassForTests>toni.com.cn.contract_sample.BaseTestClass</baseClassForTests>目的在于告知contract“引擎”将该BaseTestClass类作为所有contract测试类的基类。后续的编码中,由于服务提供者需要对自己的服务进行测试,所以是需要开发人员编写这个类(BaseTestClass),但实际上不需要在该类中实现测试方法。只需要进行一个测试服务的加载即可(本案例中服务入口的controller是FraudController),至于为什么不需要测试方法,后面会介绍。代码如下:

package toni.com.cn.contract_sample;


import toni.com.cn.contract_sample.controller.FraudController;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;

import io.restassured.module.mockmvc.RestAssuredMockMvc;
import lombok.extern.slf4j.Slf4j;

import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = toni.com.cn.contract_sample.App.class)
@Slf4j
public class BaseTestClass {

    @BeforeEach
    public void setup() {
        RestAssuredMockMvc.standaloneSetup(new FraudController());
    }
}

接下来,就是依据协商好的接口编制contract脚本,脚本的位置应该在src\test\resources的contracts目录(自建)中,contract脚本名无所谓,可以自己取。比如“service_first_sample.groovy”

脚本内容如下:

package contracts

org.springframework.cloud.contract.spec.Contract.make {
    request {
        method 'PUT'
        url '/fraudcheck'
        body([
               "client_id": $(regex('[0-9]{10}')),
               loanAmount: 99999
        ])
        headers {
            contentType('application/json')
        }
    }
    response {
        status OK()
        body([
               checkStuats: "FRAUD",
               "reason": "Amount too high"
        ])
        headers {
            contentType('application/json')
        }
    }
}

到此,测试框架和测试用例的开发就完成了。不过测试肯定通过不了,因为被测试的服务本身没有完成。

服务的代码后续会附上,这里先说明一下为什么不需要开发专门针对具体服务的测试类。这是因为contract框架在工程的install处理时,会自动前面定义的基类创建一个contract测试类,类名是ContractVerifierTest,这个类的JAVA文件是自动产生的,将会在target目录中,而不是开发者的src目录中。该类将依据contract脚本,自行生成对应的测试请求和响应对于的测试方法加入的该测试类中。并在工程install时自动执行测试。测试的内容就是依据契约模拟请求,向服务发送请求,并验证每一个响应元素。服务实现没有完成或者响应不符合预期都会断言失败,相应的如果没有断言失败则说明测试通过。比如本案例中,ContractVerifierTest将产生一个测试方法如下:

@Test
	public void validate_service_first_sample() throws Exception {
		// given:
			MockMvcRequestSpecification request = given()
					.header("Content-Type", "application/json")
					.body("{\"client_id\":\"7375534648\",\"loanAmount\":99999}");

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

		// 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("['checkStuats']").isEqualTo("FRAUD");
			assertThatJson(parsedJson).field("['reason']").isEqualTo("Amount too high");
	}

从方法名到方法内容,都是contract框架自动产生的。所以,就服务而言,开发者根本不需要额外编辑测试方法和测试类。如果想自己开发其他更复杂的测试,也可以。

从服务提供者的角度而言,针对想测试的服务,将相应的contract脚本文件编辑好就可以了,都会产生响应的测试方法到该ContractVerifierTest类中或者其基类中,不需要额外编辑其他测试类。就是这么简单。

这样的话,服务提供者就不需要服务请求者开发好来在实际集成环境中来测试,而是自己就可以进行测试了。

附:本案例服务的代码如下:

controller类

package toni.com.cn.contract_sample.controller;


import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;
import toni.com.cn.contract_sample.entity.LoanRequest;



@RestController
@Slf4j
public class FraudController {
	
	

	@PutMapping(value = "/fraudcheck", consumes="application/json", produces="application/json")
    public String check(@RequestBody LoanRequest loanRequest) { 
		log.info("请求的参数是:{}",loanRequest);
        if (loanRequest.getLoanAmount() > 10000) { 
            return "[{checkStuats: FRAUD, reason: Amount too high}]"; 
        } else {
            return "[{checkStuats: OK, reason: Amount OK}]"; 
        }
    }
}

请求对象:

package toni.com.cn.contract_sample.entity;

import com.fasterxml.jackson.annotation.JsonProperty;

public class LoanRequest {

    @JsonProperty("client.id")
    private String clientId;

    private Long loanAmount;

    public String getClientId() {
        return clientId;
    }

    public void setClientId(String clientId) {
        this.clientId = clientId;
    }

    public Long getLoanAmount() {
        return loanAmount;
    }

    public void setLoanRequestAmount(Long loanAmount) {
        this.loanAmount = loanAmount;
    }
}

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到