Testing Insights

Spring Cloud Interface Contract Testing

2025-10-29

In the microservice system, developers generally have the following methods to perform interface testing:

1. Build a complete microservice environment, run all dependent microservices, and then write test cases for the microservices to be tested.

2. Use Mock to simulate dependent microservices and database reads and writes;

3. Contract testing, service providers and consumers write their own test cases according to the same contract.

Among them, method 1 has a relatively large workload, and maintaining such an environment is also a troublesome thing, but it can truly simulate the complete process of the request. Method 2 allows the test to be focused on its own microservice, but once the dependent interface changes, Mock cannot reflect it in time, and it may not be discovered until the integration test, which is a hidden danger. Method 3 is a better method in microservice architecture, service providers and consumers can develop and test independently according to the same version of the contract at the same time, and do not need to run the whole microservice system completely, which has a certain guarantee in convenience and accuracy.

This article describes how to elegantly write interface test cases in Spring Cloud microservices, which rely on Spring Cloud Contract (contract testing framework) and DbUnit (database tool to simulate database reads and writes). A good test case should test the integrity of the interface logic without causing damage to the database (this requires using the DbUnit tool) and not dependent on other microservices when running the test case (this requires using contract testing).

First, the following example project dependencies are introduced:

  Spring Cloud:  Greenwich.RELEASE

  DbUnit: 2.6.0

The specific dependencies also need to be replaced according to the actual Spring Cloud version.

1. Use DbUnit to complete the mock at the database level

The implementation logic of the DnUnit tool is to back up the corresponding database according to the database connection information you provide, and then write the test data you prepared to the database, and then execute the test cases.

First, prepare the test data, create a testData.xml file under src/test/resources, and write the test data in the following format.

//img.enjoy4fun.com/news_icon/d40u2movterc72pmtjq0.jpeg

Suppose we have an interface http://localhost:8080/user/${userUuid} to get user information based on userUuid, the specific implementation is not listed, this is not the focus of this article, as long as this interface exists, it will return JSON data in the following format.

 //img.enjoy4fun.com/news_icon/d40u35h9q3nc72vg11kg.jpeg

Then write the test class:

 //img.enjoy4fun.com/news_icon/d40u3ngvterc72pmv660.jpeg

After the compilation run, the test case passes, you can check whether the actual database is still in the original state, if so, it means that the DbUnit tool has been successfully introduced. Of course, writing test cases similar to creating users during this process can better see whether the DbUnit is effective.

In the middle, you may encounter a problem: org.dbunit.database.AmbiguousTableNameException: EVALUATE, this is a very pit problem, I struggled with this question for two days, various Baidu googles to no avail, and finally found that it was the Spring Cloud Greenwich.RELEASE version used mysql-connector-java is version 8.0, and you need to change it to version 5.X for the DbUnit to run properly.

Once the DbUnit is running perfectly, it's time for contract testing.


2. Spring Cloud Contract

Let's talk about contracts first, for service providers, contracts can be used to constrain their unit testsUse cases, test cases written by service providers, must comply with this contract to ensure that the interfaces provided by the service provider are indeed in accordance with this contract. For service consumers, the contract can simulate what kind of result they will get when they call this microservice. Writing contracts can be done using groovy or yml, and Spring Cloud Contract can generate test cases based on this contract, which we can effectively use to simplify the writing of unit test cases for service providers.

Service Provider:

Introduce dependencies

//img.enjoy4fun.com/news_icon/d40u4q0vterc72pn0qhg.jpeg

Here is the baseClassForTests property configuration, where you declare the base class when Spring Cloud Contract automatically generates test cases, in which you need to inject the context of MockMvc (the @Before RestAssuredMockMvc.mockMvc(mvc) line in the test class sample code above).

Then write the contract, I use the yml method, Groovy is not too familiar, but using Groovy is definitely more flexible.

Spring Cloud Contract will go to the src/test/resources/contracts directory to load the contract file by default getUser.yml (The specific content of the contract file also needs to be written according to your actual interface rules, the status returned here is only suitable for my test code, you can organize a variety of different parameter submissions to simulate various complex situations to improve the code coverage of the test)

//img.enjoy4fun.com/news_icon/d40u57j8hlms72p3qj6g.png

If successful, you can see a file called XXXX-stub.jar in the target directory of the code directory, this stub file is the file you can give to the service consumer to use, you can put it in your own maven repository for others to download.

Then, you can find a ContractVerifierTest class in target\generated-test-sources, which extends the UserControllerTest class you wrote, which is the test case automatically generated based on the contract.

Service consumers:

The key for service consumers is to introduce stub files provided by service providers, and there are two ways to import them: remote and local.

First you need to introduce dependencies:

//img.enjoy4fun.com/news_icon/d40u5k99q3nc72vg4nt0.png

When introducing stubs locally, you need to obtain the service provider's code and then compile it, that is, ensure that there are corresponding stub files in the local Maven repository.

Then write the test code for the consumer:

//img.enjoy4fun.com/news_icon/d40u71h9q3nc72vg7org.png

thereinto

//img.enjoy4fun.com/news_icon/d40u7bgvterc72pn5lk0.png

This section is for local use of stub files, and if it is a remote call, it needs to be done as follows:

Start by declaring the stub dependency in the application.yml file:

//img.enjoy4fun.com/news_icon/d40u7kb8hlms72p3vajg.png

repositoryRoot to your own, and then change the StubsMode in @AutoConfigureStubRunner on the test code to REMOTE.

If the compilation and running test case passes, it means that the consumer contract test is successful, because you did not start cloud-user-service-server, but your test case still passed, and the returned value of the invoked interface is the value agreed in the contract.

Key concepts & knowledge points

1. The smaller the priority value, the higher the priority

In this case, we can use priority to set the contract of the wrong return value to a higher priority and lower the contract priority of the normal return value, which is conducive to the consumer's request to get the corresponding error when responding to the error, rather than matching the correct response result.

2. Some concepts in request

The parameters in request, whether they are queryParameters or the parameters in the body, if you don't write the corresponding matcher, the contract is a contract where the parameters must be strictly equal by default.

request.matchers is the rule used to define the request parameters, that is, the consumer must submit the parameters according to this rule; Moreover, it is best to define only the rules that must be passed in matchers, otherwise you will face parameters that do not need to be passed, and consumers must fill in this parameter when using the contract. Parameters that do not exist in the contract are passed on by default.

The type of request matchers queryParameters is available with the following values:

equal_to_json, equal_to, not_matching, matching, containing, absent, equal_to_xml

3. Some concepts in response

response.body is used to simulate the return value given to the consumer, and is also used to verify whether the return result of the test case automatically generated by Spring Cloud Contract matches this value

The usefulness of response.matchers is when the return result obtained by Spring Cloud Contract automatically generating test cases is different from the value defined in response.body, such as creating a user-generated UUID, this UUID must be random, and response.body must not be customizable, so you need to write a response.matchers definition uuid rule, as long as the actual return value meets this rule, the test case is considered to pass. matchers do not specifically define matching rules, i.e., are strictly equal to the values defined in response.body

It is best to write the values that will inevitably be returned by calling this interface in response.body, and write the corresponding matchers for fields with dynamic values corresponding to the fields returned by response.body, otherwise the automatically generated test cases will not pass

The following values are available for the response matchers type:

by_type, by_command, by_time, by_date, by_timestamp, by_null, by_equality, by_regex

Response Matchers type=by_regex, a predefined regular expression like this is available using predefined:

non_blank, iso_date_time, iso_8601_with_offset, iso_time, iso_date, only_alpha_unicode, url, hostname, any_boolean, uuid, ip_address, any_double, number, non_empty, email

4. In matchers, urls cannot be used with normal regular expressions (the key is that they cannot be used with ^ and $, including in groovy), YML can.

Limitations

1. In requests with uncertain parameters such as add/update, the request parameter can only write the required value, and the response can only write the data that will inevitably be returned (all valid fields can be updated).

2. reuqest.queryParameters cannot write parameter contracts for array types, only allow all by default. (i.e., request matchers can judge that it is not empty)

3. If each field of the parameter object in request.body can be empty, this kind of contract cannot be written, if the request.body of the contract is not written, the code will report the error that the body cannot be empty, and if it is written, it forces the field of the contract body to be passed, which is a contradiction, so you can only choose a parameter that will be passed in most cases and write it into the request.body, and pass this parameter when the consumer calls it.

Problems encountered in the process of practice

1. Unit tests can be compiled in jenkins scripts, but the test cases are completely unrunnable, and the error phenomenon is that other microservices, such as config-server, cannot be accessed.

Solution:

This problem has been bothering us for a long time, and finally it turns out that the maven image used during compilation does not join the entire Spring Cloud network environment. The overall environment is that Jenkins is started in Docker, Jenkins creates a multi-pipeline task calls the Jenkinsfile in the project code, and the pipeline is defined in the Jenkinsfile//img.enjoy4fun.com/news_icon/d40u8ep9q3nc72vga6hg.png

It can be seen that the official maven image is used for compilation work, but this image is official and has not been added to the network used by our custom SpringCloud, so adding network parameters to args can solve the problem

//img.enjoy4fun.com/news_icon/d40u96ovterc72pn8gs0.png

2. The time of DBUnit parsing is incorrect, in the xml data, the time is written in the format of "yyyy-MM-dd HH:MM:SS", and the code will automatically convert it to UTC time format.

Solution:

In fact, it is a time zone problem, jenkins is launched through the Docker container, if the Docker container is not set to the correct time zone, openjdk8 reads the time zone from the system's /etc/timezone file by default, so the time in the xml is not parsed correctly, and the default timezone is 0 time zone. The solution is to set the corresponding time and time zone when starting jenkins, such as docker-compose.yml:

//img.enjoy4fun.com/news_icon/d40u9ob8hlms72p42f6g.png

If the timezone file does not exist on the host, you can create one yourself:

//img.enjoy4fun.com/news_icon/d40u9vr8hlms72p42qig.png

more stories
See more