Introduction to the

Usage scenarios

It is mainly used for CDC (consumer-driven contract) testing in microservices architecture. The following figure shows multiple microservice calls. How do we test if we change a module?

  • Two traditional test ideas

    • Deploy all microservices in a simulated production environment, and then test them
      • advantages
        • The test results are highly reliable
      • disadvantages
        • It’s too expensive to test, it’s time consuming, it’s labor-intensive, it’s machine intensive to set up an environment
    • Mock other microservices to do end-to-end testing
      • advantages
        • There is no need to install a whole set of products, and the measurement is convenient and fast
      • disadvantages
        • There are many Mock services to write and a lot of different versions of them to maintain, which can also be time consuming
  • Spring Cloud Contrct solution

    • Each service produces a verifiable Stub Runner, which is called by WireMock, and both parties contract to update their stubs as one changes and test the other. Stubs simply provide data, or contracts, that can be used lightly to simulate service request returns. Mocks add validation on top of stubs

Contract Testing process

  • Service provider
    • You can write contracts using either Groovy DSL scripts or YAML files
    • Write test base classes for plug-ins to automatically generate test cases during the build process
    • The generated test cases run automatically and fail if we provide services that do not meet the rules in the contract
    • The provider continues to refine the functionality until the service meets the contract requirements
    • Publish the Jar package, and publish the Jar with the Stub suffix
  • Service consumer
    • Write test cases for interfaces that rely on external services
    • Specify the Stub JAR package that needs to depend on the service through annotations
    • Verify that there is no problem with the external service

A simple case

Service provider

Simulate a user service

The project address

cloud-contract-provider-rest

Project depend on

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

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-contract-maven-plugin</artifactId>
      <extensions>true</extensions>
      <configuration>
        <! Base class for automatic test case generation by plug-ins during build
        <baseClassForTests>
          com.github.freshchen.keeping.RestBaseCase
        </baseClassForTests>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>
Copy the code

Write a contract

Since the consumer is driving the contract, you first need to establish a contract that tells the consumer what stubs to provide, and generate unit tests to verify the provider’s ability to meet the contract

Contract.make {
    description "add user"

    request {
        method POST()
        url "/user"
        body([
                age: value(
                        // If the consumer wants to create a user of any age, it gets "success: true" in response.
                        consumer(regex(number())),
                        // The test generated by the provider calls the interface to create a user aged 20
                        producer(20)
                )

        ])
    }

    response {
        status OK()
        headers {
            contentType applicationJson()
        }
        // The default return provided to the consumer
        body([
                success: true
        ])

        // Provide the rules that the body needs to meet during testing
        bodyMatchers {
            // The custom model has a SUCCESS field. ByEquality can verify that success is true in the JSON returned by the server
            jsonPath '$.success', byEquality()
            The assertIsTrue method can be implemented in the base class
            jsonPath '$.success', byCommand('assertIsTrue($it)')}}}Copy the code

The test base class

@SpringBootTest
@RunWith(SpringRunner.class)
public abstract class RestBaseCase {

    @Autowired
    WebApplicationContext webApplicationContext;

    @Before
    public void setup(a) {
        MockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(webApplicationContext);
        RestAssuredMockMvc.standaloneSetup(builder);
    }
    
    protected void assertIsTrue(Object object) {
        Map map = (Map) object;
        assertThat(map.get("success")).isEqualTo(true); }}Copy the code

Realize the function

@Data
@ApiModel
public class JsonResult<T> {
    @NonNull
    @APIModelProperty (" Successful or not ")
    private boolean success;
    
    @apiModelProperty (" Response result ")
    private Optional<T> data = Optional.empty();

    @APIModelProperty (" Error code ")
    private Optional<Integer> errCode = Optional.empty();

    @APIModelProperty (" Error message ")
    private Optional<String> errMessage = Optional.empty();

}
Copy the code
@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping
    public JsonResult create(@RequestBody User user) {
        returnJsonResult.ok(); }}Copy the code
server.port=8880
Copy the code

test

To implement our service functionality, the specific code logic can be viewed in the project address and then tested to see if it complies with the contract

mvn clean test

You can find the generated-test-sources directory in the target directory, where the case generated and run automatically by the plug-in for us

public class ContractVerifierTest extends RestBaseCase {

	@Test
	public void validate_addUser(a) throws Exception {
		// given:
			MockMvcRequestSpecification request = given()
					.body("{\"age\":20}");

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

		// then:
			assertThat(response.statusCode()).isEqualTo(200);
			assertThat(response.header("Content-Type")).matches("application/json.*");

		// and:
			DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());

		// and:
			assertThat(parsedJson.read("$.success", Boolean.class)).isEqualTo(true);
			assertIsTrue(parsedJson.read("$.success")); }}Copy the code

release

If all goes well, you can deploy and unpack the published STUbs to see the JSON defined for the consumer

{
  "id" : "737fc339-a9c5-41f4-909a-a783dbc0855f"."request" : {
    "url" : "/user"."method" : "POST"."bodyPatterns": [{"matchesJsonPath" : "$[?(@.['age'] =~ /-?(\\d*\\.\\d+|\\d+)/)]"}},"response" : {
    "status" : 200."body" : "{\"success\":true}"."headers" : {
      "Content-Type" : "application/json"
    },
    "transformers" : [ "response-template"]},"uuid" : "737fc339-a9c5-41f4-909a-a783dbc0855f"
}
Copy the code

Service consumer

To reserve the service, the user service interface will be adjusted

The project address

cloud-contract-consumer-rest

The service call

The service caller will call port 8880, the user service above

public interface UserApi {

    @POST("/user")
    Call<JsonResult> create(@Body User user);
}

public class UserClient {

    public static JsonResult createUser(User user) throws IOException {
        UserApi userApi = new Retrofit.Builder().baseUrl("http://127.0.0.1:8880")
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(UserApi.class);
        returnuserApi.create(user).execute().body(); }}Copy the code

The validation service

Even if the user service is not developed, with STUBS, appointments can be developed in parallel and completed without blocking even if they rely on the user service interface

@SpringBootTest
@RunWith(SpringRunner.class)
@AutoConfigureStubRunner( ids = {"com.github.freshchen.keeping:cloud-contract-provider-rest:+:stubs:8880"}, stubsMode = StubRunnerProperties.StubsMode.LOCAL )
public class UserClientTest {

    @Test
    public void createUser(a) throws IOException {
        User user = new User();
        user.setAge(123);
        JsonResult user1 = UserClient.createUser(user);
        BDDAssertions.then(user1.getSuccess()).isEqualTo(true); }}Copy the code

Check the log to make sure stuBS is in effect. You can see that the stub ID matches the UUID in json above

2020-12-12 1609:08.070  INFO 18224 --- [p1001114349-254] WireMock                                 : Request received:
127.0. 01. - POST /user

Connection: [keep-alive]
User-Agent: [okhttp/3.148.]
Host: [127.0. 01.:8880]
Accept-Encoding: [gzip]
Content-Length: [11]
Content-Type: [application/json; charset=UTF-8]
{"age":123}


Matched response definition:
{
  "status" : 200."body" : "{\"success\":true}"."headers" : {
    "Content-Type" : "application/json"
  },
  "transformers" : [ "response-template" ]
}

Response:
HTTP/1.1 200
Content-Type: [application/json]
Matched-Stub-Id: [737fc339-a9c5-41f4-909a-a783dbc0855f]
Copy the code