Kube Cloud Pt6 | Break the Contract from a Consumer Change

Kube Cloud Pt6 | Break the Contract from a Consumer Change

Kube Cloud Pt6 | Contract Testing

full course
  1. Kube Cloud Pt6 | Contract Testing
  2. Kube Cloud Pt6 | Consumer Contract Tests for REST Endpoints
  3. Kube Cloud Pt6 | Provider Contract Test for REST Endpoints
  4. Kube Cloud Pt6 | Fulfill the Consumer Contract Test for REST Endpoint
  5. Kube Cloud Pt6 | Break the Contract from a Consumer Change
  6. Kube Cloud Pt6 | Synchronous Contract Testing Conclusion

We’re going to go back to cloud-application at this point and update our client to expect a field that isn’t being currently provided (message generated date). This should break our contract test and prevent us from deploying, but there’s some updates we need to make to support this.

Add the New Feature to the Consumer

Go to the cloud-application project and start a new branch

$ git checkout -b generated-date
Switched to a new branch 'generated-date'

We’re using dates, so lets make sure that we have the correct serializers in the pom.xml

        <!-- dates -->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>

And we’re going to have to fix the logging aspect at src/main/java/com/bullyrooks/cloud_application/config/LoggingAspect.java

    ObjectMapper om;

    @Autowired
    public LoggingAspect() {
        om = new ObjectMapper()
                .registerModule(new JavaTimeModule());
    }

Essentially we’re going to add the java time serialization module to our json object mapper

Make a minor update to src/main/resources/application.yml

spring:
  application:
    name: cloud-application
  jackson:
    serialization:
      write-dates-as-timestamps: false

This is just something I use to make sure that my timestamps are written out correctly, its not directly applicable here, but since we’re messing with java8 dates, its a safe thing to do

Now add our new field to

  • src/main/java/com/bullyrooks/cloud_application/controller/dto/CreateMessageResponseDTO.java
  • src/main/java/com/bullyrooks/cloud_application/message_generator/client/dto/MessageResponseDTO.java
  • src/main/java/com/bullyrooks/cloud_application/service/model/MessageModel.java
private Instant generatedDate;

Add the new date to the test data in src/test/resources/json/message-generator-response.json

"message": "All dwarfs are bastards in their father's eyes",
  "generatedDate": "2022-03-07T18:37:54.124523300Z"

Now we need to make a new mapper at src/main/java/com/bullyrooks/cloud_application/message_generator/mapper/MessageGeneratorMapper.java

package com.bullyrooks.cloud_application.message_generator.mapper;

import com.bullyrooks.cloud_application.message_generator.client.dto.MessageResponseDTO;
import com.bullyrooks.cloud_application.service.model.MessageModel;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;

@Mapper
public interface MessageGeneratorMapper {
    MessageGeneratorMapper INSTANCE = Mappers.getMapper(MessageGeneratorMapper.class);

    MessageModel messageResponseToMessage(@MappingTarget MessageModel in, MessageResponseDTO dto);
}

And the corresponding updates to the service class at src/main/java/com/bullyrooks/cloud_application/service/MessageService.java

            messageModel = MessageGeneratorMapper.INSTANCE.messageResponseToMessage(messageModel, dto);
            genMsgSuccess.increment();
            log.info("retrieved message: {}", messageModel.getMessage());
        } else {
            //if they provided a message, return now
            messageModel.setGeneratedDate(Instant.now());
        }

So instead of copying fields over individually (for message and generatedDate) the mapper will take in the original message and then add the fields that are missing from the dto object. If we passed in a message, we want to return a new generatedDate.

Now update the behavior test at src/test/java/com/bullyrooks/cloud_application/controller/MessageControllerTest.java

    @AfterEach
    void cleanup(){
        //cleanup
        outputDestination.clear("message.created");
    }

...
 @Test
    void testSaveMessage() throws IOException {
        Long userId = 1l;

        //given
        Instant testStart = Instant.now();
        CreateMessageRequestDTO request = CreateMessageRequestDTO
                .builder()
...
        assertEquals(request.getMessage(), dto.getMessage());
        assertTrue(dto.getGeneratedDate().isAfter(testStart));
...
@Test
    void testGetReturnsMessageIfMissing() throws InterruptedException, IOException {
        Long userId = 1l;

        //given
        Instant testStart = Instant.now();
        CreateMessageRequestDTO request = CreateMessageRequestDTO
                .builder()
...
        when(messageGeneratorClient.getMessage()).thenReturn(
                MessageResponseDTO.builder()
                .message(faker.gameOfThrones().quote())
                        .generatedDate(Instant.now())
                .build());
...
        assertTrue(StringUtils.isNotBlank(dto.getMessage()));
        assertTrue(dto.getGeneratedDate().isAfter(testStart));

I’m adding a cleanup method to the kafka testing logic because I found that if my tests were failing data from the previous test was polluting the topic for the next test. Next, I’m adding a ‘test start’ date and confirming that the message I get back from the endpoint is after that date. Additionally, I’m updating the mock so that it will return a generatedDate field.

Finally the contract test at src/test/java/com/bullyrooks/cloud_application/message_generator/client/MessageGeneratorClientTest.java

...
    public void generateMessage() {
        MessageResponseDTO response = messageGeneratorClient.getMessage();
        assertTrue(StringUtils.isNotBlank(response.getMessage()));
        assertTrue(null != response.getGeneratedDate());
    }
...

No major changes here… just that I’m expecting the date that my json is now returning. Keep in mind that I have to manually update two tests which is what I warned about before. If I didn’t update the contract, my tests would pass and I would think that I’m good to deploy, but I’m not (message-generator doesn’t support this functionality yet)

Go ahead and add/commit and push this up. Go to github cloud-application repository and create a new pull request for this change. This should fail, since the provider doesn’t have this field.

Update the Provider to Fulfill the Contract

Switch back over to message-generator and add the code needed to fulfill the contract.

Add generatedDate to MessageResponseDTO and MessageModel (I may have refactored the MessageService to return a MessageModel object that looks like this)

package com.bullyrooks.messagegenerator.service.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.Instant;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MessageModel {

    private String message;
    private Instant generatedDate;
}

Update MessageService to return the generatedDate

    public MessageModel getMessage(){
        MessageModel model = MessageModel.builder()
                .message(faker.gameOfThrones().quote())
                .generatedDate(Instant.now())
                .build();

        return model;
    }

Update MessageControllerContractIT test so that it can fulfill the contract

    @State("generator creates a message")
    public void shouldReturnMessage() {
        //@formatter:off
        MessageModel dto = MessageModel.builder()
                .message("All dwarfs are bastards in their father's eyes")
                .generatedDate(Instant.parse("2022-03-07T18:37:54.124523300Z"))
                .build();
        Mockito.when(service.getMessage()).thenReturn(dto);
        //@formatter:on
    }

and update MessateControllerTest to expect the generatedDate field

        //Verify request succeed
        assertEquals(200, result.getStatusCodeValue());
        MessageResponseDTO response = result.getBody();
        log.info("Test message returned: {}",response.getMessage());
        assertTrue(StringUtils.isNotBlank(response.getMessage()));
        assertNotNull(response.getGeneratedDate());

Go ahead and push your feature branch and create the pull request in message-generator. This should pass so you can merge. If it does, go ahead and do so

Rebuild the Consumer

Switch back over to your cloud-application pull request, click on the failed build and restart it as we did before (wait for the provider build to main to succeed first just in case, it should work without needing to wait though)

This build should now succeed and you can view the successful build in the pull request as well as the contract fulfillment via pactflow

Go ahead an merge the pull request now.

That’s it! We’re now successfully blocking breaking builds from the consumer application. We can also make a breaking change on the provider side, but its essentially the same process (i.e. remove the generatedDate that we just added) and the change will not make it through the pull request or will it actually deploy if someone forces the change to main.

0 comments on “Kube Cloud Pt6 | Break the Contract from a Consumer ChangeAdd yours →

Leave a Reply

Your email address will not be published. Required fields are marked *