In the article “Get Your Microservices in Sync with Consumer-Driven Contract Testing”, we saw how easy it is to use the stubs generated with Spring Cloud Contract when working in the JVM world. But, what happens if we want to use them in a consumer written in a different language?

In real life, a remote control doesn’t need to be made by the same company as the TV to work—it just needs to follow the same protocol. The same thing happens if the client consuming the stubs is built in another language, such as a frontend application using Angular.

Use case

Let’s look at a use case where we need to develop a frontend application with Angular that consumes a service exposed by a REST API developed with Spring Boot 3.

Functional requirements

A web application must be developed with Angular for the Back Office, supporting the following operations:

Non-functional requirements

These requirements are meant to ensure seamless integration between frontend and backend, maintaining contract stability throughout development.

  1. Development will be done in parallel by two teams:
  1. The contract should be defined between the two teams, with the frontend team stating their requirements and the backend team finding the optimal way to provide the data.
  2. The backend team will generate the stubs based on the defined contract and will publish them through their CI/CD pipeline to a public repository (Artifactory or Maven), where the consumer team can access them.
  3. The frontend team will use the stubs with Docker for their integration tests.
  4. If either team needs to change the contract, they should raise their hand and call a meeting to refine the contract. Once the changes are agreed upon, the backend team will update the contract, regenerate the stubs, publish them to the repository, and both teams will continue development in parallel.

Let’s get to work

Contract definition

Based on the functional requirements, the frontend team decides that they’ll need a REST API with the following endpoints:

Each product will contain the following fields:

{
 "id": 17035535,
 "name": "Cheap product",
 "price": {
   "amount": "9.99",
   "currency": "EUR"
 }
}

The two teams have also agreed on the behavior of the stubs when calling the endpoints:

Create the producer

Let’s go to Josh Long’s favorite page and generate the producer including the following dependencies:

We generate the producer in Spring Initializr including the following dependencies: Spring Web, Contract Verifier, Spring Data JPA, H2 Database, Lombok

We generate the project and extract its content into the catalog module.

Create the contracts

In the path catalog/src/test/resources/ we will have a contracts folder. If it doesn’t exist, we create it, because this is where we will add our contracts.

We can write the contracts in Java or use one of the DSLs. Spring Cloud Contract offers two types of DSLs: one written in Groovy and another in YAML. My favorite is the YAML format for defining contracts, because it’s easier to read and understand.

  1. Return the list of products

The stub will always return a list of products defined in the file products.json:

{
 "products": [
   {
     "id": "17035535",
     "name": "Cheap product",
     "price": {
       "amount": "9.99",
       "currency": "EUR"
     }
   },
   {
     "id": "17005954",
     "name": "Quality product",
     "price": {
       "amount": "29.99",
       "currency": "EUR"
     }
   }
 ]
}

And this would be the contract defined in shouldReturnProducts.yml:

description: Products list
request:
   method: GET
   url: /api/products
response:
   status: 200
   headers:
       Content-Type: application/json
   bodyFromFile: response/products.json
  1. Return the product

As agreed, this would be the shouldReturnProductById.yml contract:

name: Get existing product
request:
 method: GET
 url: /api/products/17035535
response:
 status: 200
 headers:
   Content-Type: application/json
 body:
   id: 17035535
   name: Cheap product
   price:
     amount: 9.99
     currency: EUR


---
name: Get non-existing product
request:
 method: GET
 url: /api/products/99999999
response:
 status: 404
  1. Create a new product.

In this contract, the product name is a mandatory field and cannot be empty. Therefore, if the product has all fields reported, status code 204 will be returned, but if the product name is not reported or is an empty string, status code 400 will be returned. Let's put it in shouldCreateProduct.yml:

name: Create product
request:
 method: POST
 url: /api/products
 headers:
   Content-Type: application/json
 body:
   name: Not so cheap product
   price:
     amount: 19.99
     currency: EUR
response:
 status: 204


---
name: Create product without name
request:
 method: POST
 url: /api/products
 headers:
   Content-Type: application/json
 body:
   name:
   price:
     amount: 19.99
     currency: EUR
response:
 status: 400
  1. Update a product.

If the product exists (has id = 17035535), the product will be updated. Otherwise, it will return a ‘Not Found’. This is the shouldUpdateProduct.yml contract:

name: Update product
request:
 method: PUT
 url: /api/products/17035535
 headers:
   Content-Type: application/json
 body:
   name: Inexpensive product
   price:
     amount: 10.99
     currency: EUR
response:
 status: 204


---
name: Try to update non-existing product
request:
 method: PUT
 url: /api/products/99999999
 headers:
   Content-Type: application/json
 body:
   name: Inexpensive product
   price:
     amount: 10.99
     currency: EUR
response:
 status: 404
  1. Delete a product.

If the product exists (has id = 17005954), the product will be deleted. Otherwise, it will return a "Not Found". The product will not be truly deleted, i.e. the same call will always return the same response. This is the shouldDeleteProduct.yml contract:

name: Delete existing product
request:
 method: DELETE
 url: /api/products/17005954
 headers:
   Content-Type: application/json
response:
 status: 204


---
name: Try to delete non-existing product
request:
 method: DELETE
 url: /api/products/99999999
 headers:
   Content-Type: application/json
response:
 status: 404

Configure the contract tests

We need to configure the main class of the contract tests so that they can run correctly by configuring RestAssuredMockMvc, which integrates Rest Assured with Spring's MockMvc, allowing REST API tests to be performed directly on Spring MVC controllers without the need to deploy the application on a real server.

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class BaseTestClass {


   @Autowired
   private ProductController productController;


   @BeforeEach
   public void setup() {
       StandaloneMockMvcBuilder standaloneMockMvcBuilder = MockMvcBuilders.standaloneSetup(productController);
       RestAssuredMockMvc.standaloneSetup(standaloneMockMvcBuilder);
   }
}

Once the BaseTestClass.java is created, we need to configure it to work with the Maven plugin for Spring Cloud Contract.

<plugin>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-contract-maven-plugin</artifactId>
 <version>4.2.1</version>
 <extensions>true</extensions>
 <configuration>
  <testFramework>JUNIT5</testFramework>
  <baseClassForTests>com.paradigmadigital.catalog.BaseTestClass</baseClassForTests>
 </configuration>
</plugin>

Generate stubs

We can now generate the stubs without having implemented the business logic with the following command from the catalog folder:

mvn clean package -DskipTests

It’s important to skip the tests because we haven’t implemented any code yet.

Publish the contracts

There is a Docker project that can run a mock server with the stubs created by Spring Cloud Contract, but for our requirements it started up too slowly. For this reason, we created our own Docker Spring Cloud Contract Stub Runner, which builds a Docker image published on DockerHub. We use it as the base for the images we're creating in the CI/CD pipeline with integrated stubs. This way, the Docker image starts up almost instantly, 10 times faster than the original method. You can find documentation on how to use it and an example here.

Let’s create an image with the stubs we’ve generated. This is the content of our StubrunnerDockerfile:

FROM ismail2ov/spring-cloud-contract-stub-runner:3.1.1

LABEL Author="Ismail Ahmedov <i.a.ismailov@gmail.com>"

WORKDIR /home/scc

ENV STUBS_GROUP_ID com.paradigmadigital
ENV STUBS_ARTIFACT_ID catalog
ENV STUBS_VERSION 0.0.1-SNAPSHOT
ENV STUBS_FOLDER com/paradigmadigital

ENV STUBRUNNER_PORT 8080
ENV STUBRUNNER_STUBS_MODE LOCAL
ENV STUBRUNNER_IDS ${STUBS_GROUP_ID}:${STUBS_ARTIFACT_ID}:${STUBS_VERSION}:stubs:${STUBRUNNER_PORT}

ENV REPOSITORY_FOLDER .m2/repository/${STUBS_FOLDER}/${STUBS_ARTIFACT_ID}/${STUBS_VERSION}/

RUN mkdir -p ${REPOSITORY_FOLDER}
COPY --chown=scc:scc ./target/${STUBS_ARTIFACT_ID}-${STUBS_VERSION}-stubs.jar ${REPOSITORY_FOLDER}

ENTRYPOINT ["./run.sh"]

Now we can create the Docker image with the command:

docker build -f StubrunnerDockerfile --tag paradigmadigital/ecommerce-catalog-stubs .

We can also run the created image with the command:

docker run -d --rm  --name ecommerce-catalog-stub-server -p 8080:8080 paradigmadigital/ecommerce-catalog-stubs

And test them by entering this URL in your browser: https://www.paradigmadigital.com/api/products.

ℹ️ In our case, we had a step in the CI/CD pipeline that created the Docker image and pushed it to the company's Docker Registry.

This way, both teams can work in parallel.

Create the consumer

The frontend team has been working and has finished their development. In order to run it, we need to switch to the ecommerce folder and install the dependencies:

npm install

Once we have all the dependencies installed, run the integration tests:

npm run integration-tests

We can see that 4 Test Suites were executed, containing 13 tests, and all of them passed successfully.

4 Test Suites were executed, containing 13 tests, and all of them passed successfully in 4.36s

The configuration of the Docker image with the stub is done in the run-integration-tests.js file:

const STUB_IMAGE = 'paradigmadigital/ecommerce-catalog-stubs:latest';

Conclusions

In this article, we explored the optimal way to integrate stubs generated by a backend team using Spring Cloud Contract into a frontend project with Angular for contract testing.

We saw how to use a Docker image to run a stub runner with the stubs retrieved from Artifactory or a Git repository. Since that process was a bit slow, we created a Docker image that already contains the stubs and runs in just a few seconds.

You can check out all the code with step-by-step commits in the GitHub repository: Angular example with Docker Spring Cloud Contract Stub Runner.

Tell us what you think.

Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.

Subscribe