Kube Cloud Pt6 | Provider Contract Test for REST Endpoints

Kube Cloud Pt6 | Provider Contract Test for REST Endpoints

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

Now lets move over to message-generator repo and create a new branch

git checkout -b provider-test

Pom Updates for Dependencies

Update the pom.xml to pull in both the provider test library and the plugin

...
        <pact.version>4.3.5</pact.version>
        <maven-failsafe-plugin.version>3.0.0-M5</maven-failsafe-plugin.version>
        <maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
        <pact.maven.plugin.version>4.3.5</pact.maven.plugin.version>
    </properties>
...
    <profiles>
        <profile>
            <id>contract</id>
            <build>
                <plugins>
                    <plugin>
                        <artifactId>maven-surefire-plugin</artifactId>
                        <version>${maven-surefire-plugin.version}</version>
                        <configuration>
                            <excludedGroups>UnitTest</excludedGroups>
                        </configuration>
                    </plugin>
                    <plugin>
                        <artifactId>maven-failsafe-plugin</artifactId>
                        <version>${maven-failsafe-plugin.version}</version>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>integration-test</goal>
                                    <goal>verify</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>

                </plugins>
            </build>
        </profile>
    </profiles>
...

        <!-- Contract Testing -->
        <dependency>
            <groupId>au.com.dius.pact.consumer</groupId>
            <artifactId>junit5</artifactId>
            <version>${pact.version}</version>
        </dependency>
        <dependency>
            <groupId>au.com.dius.pact.provider</groupId>
            <artifactId>junit5spring</artifactId>
            <version>${pact.version}</version>
        </dependency>
...
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${maven-surefire-plugin.version}</version>
                <configuration>
                    <excludedGroups>ContractTest</excludedGroups>
                </configuration>
            </plugin>
            <plugin>
                <groupId>au.com.dius.pact.provider</groupId>
                <artifactId>maven</artifactId>
                <version>${pact.maven.plugin.version}</version>
                <configuration>
                    <pactBrokerUrl>${env.PACTFLOW_URL}</pactBrokerUrl>
                    <pactBrokerToken>${env.PACTFLOW_TOKEN}</pactBrokerToken>
                    <pactBrokerAuthenticationScheme>Bearer</pactBrokerAuthenticationScheme>
                </configuration>
            </plugin>

This time we’re doing quite a few things that may be new (they were to me).

First, we’re adding the dependencies to create the pact provider tests. We need these in order to get access to the classes that we’ll use to write the provider side tests (which are significantly different than the consumer tests). Additionally, we need the pact provider plugin. This gives us access to the functions we can call through maven to publish the test verifications as well as confirm that the contract has been met before we can deploy.

Next, pact provider tests introduce a dependency on the pact broker. We don’t necessarily want our unit tests to have to call out to pact broker every time we want to build and test a minor change that doesn’t affect the contract. In fact, we want to limit those executions because they’re expensive and have an environment dependency (a connection to the pact broker). So we’re introducing the surefire plugin configuration to ignore certain tests (annotated with the junit Tag annotation) and a new profile which will run those tests using a profile flag when we actually want them executed.

Tests

Lets first create the provider verification test in src/test/java/com/bullyrooks/messagegenerator/controller/MessageControllerContractIT.java

package com.bullyrooks.messagegenerator.contract;

import au.com.dius.pact.provider.junit5.HttpTestTarget;
import au.com.dius.pact.provider.junit5.PactVerificationContext;
import au.com.dius.pact.provider.junitsupport.IgnoreNoPactsToVerify;
import au.com.dius.pact.provider.junitsupport.Provider;
import au.com.dius.pact.provider.junitsupport.State;
import au.com.dius.pact.provider.junitsupport.loader.PactBroker;
import au.com.dius.pact.provider.junitsupport.loader.PactBrokerAuth;
import au.com.dius.pact.provider.junitsupport.loader.VersionSelector;
import au.com.dius.pact.provider.spring.junit5.PactVerificationSpringProvider;
import com.bullyrooks.messagegenerator.controller.dto.MessageResponseDTO;
import com.bullyrooks.messagegenerator.service.MessageService;
import com.bullyrooks.messagegenerator.service.model.MessageModel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.web.server.LocalServerPort;

import java.time.Instant;


@Provider(MessageControllerContractIT.PROVIDER)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@PactBroker(
        authentication = @PactBrokerAuth(token = "${PACTFLOW_TOKEN}"),
        consumerVersionSelectors = {
                @VersionSelector(tag = "${PACT_CONSUMER_SELECTOR_TAG:okteto}")
        }
)

@AutoConfigureMockMvc
@Tag("ContractTest")
@IgnoreNoPactsToVerify
public class MessageControllerContractIT {
    final static String PROVIDER = "message-generator";
    @MockBean
    MessageService service;
    @LocalServerPort
    private int port;

    @BeforeEach
    void setup(PactVerificationContext context) {
        if (null!=context) {
            context.setTarget(new HttpTestTarget("localhost", port));
        }
    }

    @TestTemplate
    @ExtendWith(PactVerificationSpringProvider.class)
    void pactVerificationTestTemplate(PactVerificationContext context) {
        if (null!=context) {
            context.verifyInteraction();
        }
    }


    @State("generator creates a message")
    public void shouldReturnMessage() {
        //@formatter:off
        MessageModel dto = MessageModel.builder()
                .message("All dwarfs are bastards in their father's eyes")
                .build();
        Mockito.when(service.getMessage()).thenReturn(dto);
        //@formatter:on
    }
}

First, set our tag (ContractTest) and our pact configuration to setup a pact provider test that should talk to the pactflow pact broker. We’re going to inject our token into the environment variables later. Next we’re letting the test know to setup a pact server in @BeforeEach. This server is going to take our contract, setup a client to make the requests the contract specifies and verify the response against the contract. That’s what we’re doing with the PactVerificationSpringProvider (verifying the contract against the response). Finally, we’re defining a @State test (which should match the state from the contract). The only thing we have to do here is tell our controller to interact with our mock MessageService and force it to return the expected response to match the contract.

Its actually really easy, but the hard part is figuring out where to mock and defining the behavior to return. Some interactions are basic (like this one), but some contract tests may drive you to use more of your actual service behavior (like a behavior driven test). The line between black box tests (behavior tests) and contract tests will be blurred. I prefer to keep them separate because they perform separate functions and contract tests require environment configuration. However, that introduces a risk that your contract test behavior could drift from your behavior tests and the contract tests could pass, but your service doesn’t actually exhibit the behavior they say they do. You will have to find the balance for your team and organization.

In all other unit tests add this annotation

@Tag("UnitTest")

This will keep the contract test verification step from running them.

You should be able to confirm both types of tests now (unit tests):

mvn clean test

should run successfully since it shouldn’t use any of the new contract test logic.

Before we run the contract tests you’ll need to set the environment variables.

export PACTFLOW_TOKEN=<your token>
export PACTBROKER_HOST=bullyrooks.pactflow.io
mvn verify -Pcontract

Github Action Updates

Make sure to add a secret for the PACTFLOW_TOKEN in the message-generator repository similar to what you did in the last course.

update features.yaml

name: Cloud Application Feature Branch Build

on:
  push:
    branches-ignore:
      - main
env:
  PACTFLOW_URL: https://bullyrooks.pactflow.io
  PACTFLOW_TOKEN: ${{secrets.PACTFLOW_TOKEN}}
  PACTBROKER_HOST: bullyrooks.pactflow.io

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # Shallow clones should be disabled for a better relevancy of analysis
      - name: Set environment variables
        run: |
          # Short name for current branch. For PRs, use target branch (base ref)
          GIT_BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
          echo "GIT_BRANCH=$GIT_BRANCH" >> $GITHUB_ENV
          echo "VERSION=$GITHUB_SHA"
          echo "VERSION=$GITHUB_SHA" >> $GITHUB_ENV

      - name: Set up JDK 11
        uses: actions/[email protected]
        with:
          java-version: '11'
          distribution: 'adopt'
          cache: maven

      - name: Maven Tests
        run: ./mvnw -B test


      - name: Create Pact Version Tag
        run: |
          ./mvnw pact:create-version-tag \
          -Dpacticipant=message-generator \
          -DpacticipantVersion=${{ env.VERSION }} \
          -Dtag=initial

      - name: Verify Contract Tests
        run: |
          ./mvnw -B verify -Pcontract \
          -Dpactbroker.host=${{env.PACTBROKER_HOST}} \
          -Dpact.verifier.publishResults=true \
          -Dpact.provider.version=${{env.VERSION}} \
          -Dpact.provider.branch=${{env.GIT_BRANCH}} \
          -Dpact.provider.tag=okteto \
          -DPACT_CONSUMER_SELECTOR_TAG=okteto,pre-okteto
        continue-on-error: true #verify may fail, but we're relying on can-i-deploy to control deploy

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Cache Docker layers
        uses: actions/cache@v2
        with:
          path: /tmp/.buildx-cache
          # Key is named differently to avoid collision
          key: ${{ runner.os }}-multi-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-multi-buildx

      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          context: .
          push: false
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
        # This ugly bit is necessary if you don't want your cache to grow forever
        # till it hits GitHub's limit of 5GB.
        # Temp fix
        # https://github.com/docker/build-push-action/issues/252
        # https://github.com/moby/buildkit/issues/1896
      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

      - name: Pact Can-i-deploy
        run: |
          docker run --rm pactfoundation/pact-cli:latest \
          broker can-i-deploy \
          --pacticipant=message-generator  \
          --to-environment=okteto \
          --version=${{ env.VERSION }}  \
          --broker-base-url=${{ env.PACTFLOW_URL }} \
          --broker-token=${{ env.PACTFLOW_TOKEN }}

First, set the environment variables we’ll need. Then I renamed the maven build task to signal that I was just running unit tests. Next I am running the contract tests in a separate task. You can see that the results are being published and that I’m running can I deploy at the end. However, there’s an important piece here where I’m creating a version tag. This signals to pact that I’ve got a new service and since our service is a provider it will be able to be promoted without needing to fulfill a contract if there’s no consumers. If we don’t have this piece we’ll never be able to deploy since we won’t have a fulfilled contract. Publishers should be able to publish if no one depends on them yet and that’s what we’re allowing.

Next onto the main.yaml

name: Cloud Application Feature Branch Build

on:
  push:
    branches:
      - main
env:
  PACTFLOW_URL: https://bullyrooks.pactflow.io
  PACTFLOW_TOKEN: ${{secrets.PACTFLOW_TOKEN}}
  PACTBROKER_HOST: bullyrooks.pactflow.io

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # Shallow clones should be disabled for a better relevancy of analysis

      - name: Set up JDK 11
        uses: actions/[email protected]
        with:
          java-version: '11'
          distribution: 'adopt'
          cache: maven

      - name: find current version
        uses: actions-ecosystem/action-get-latest-tag@v1
        id: get-latest-tag

      - name: get current version tag
        run: |
          echo "VERSION=${{ steps.get-latest-tag.outputs.tag }}" >> $GITHUB_ENV
          echo ${{ env.VERSION }}

      - name: Echo current version tag
        run: |
          echo $VERSION
          echo ${{ env.VERSION }}

      - name: Bump version
        run: |
          git config --global user.email "[email protected]"
          git config --global user.name "Actions"
          git fetch --tags
          wget -O - https://raw.githubusercontent.com/treeder/bump/master/gitbump.sh | bash
          echo "VERSION=$(git tag --sort=-v:refname --list "v[0-9]*" | head -n 1 | cut -c 2-)" >> $GITHUB_ENV
          echo ${{ env.VERSION }}

      - name: Maven Tests
        run: ./mvnw -B test

      - name: Create Pact Version Tag
        run: |
          ./mvnw pact:create-version-tag \
          -Dpacticipant=message-generator \
          -DpacticipantVersion=${{ env.VERSION }} \
          -Dtag=initial

      - name: Verify Contract Tests
        run: |
          ./mvnw -B verify -Pcontract \
          -Dpactbroker.host=${{env.PACTBROKER_HOST}} \
          -Dpact.verifier.publishResults=true \
          -Dpact.provider.version=${{env.VERSION}} \
          -Dpact.provider.branch=main \
          -Dpact.provider.tag=okteto \
          -DPACT_CONSUMER_SELECTOR_TAG=okteto,pre-okteto
        continue-on-error: true #verify may fail, but we're relying on can-i-deploy to control deploy

      - name: Pact Can-i-deploy
        run: |
          docker run --rm pactfoundation/pact-cli:latest \
          broker can-i-deploy \
          --pacticipant=message-generator  \
          --to-environment=okteto \
          --version=${{ env.VERSION }}  \
          --broker-base-url=${{ env.PACTFLOW_URL }} \
          --broker-token=${{ env.PACTFLOW_TOKEN }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to canister.io
        uses: docker/login-action@v1
        with:
          registry: cloud.canister.io:5000
          username: ${{ secrets.CANISTER_USERNAME }}
          password: ${{ secrets.CANISTER_PASSWORD }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v3
        with:
          images: cloud.canister.io:5000/bullyrooks/message-generator
          #setting value manually, but could come from git tag
          tags: |
            type=ref,event=tag

      - name: Cache Docker layers
        uses: actions/cache@v2
        with:
          path: /tmp/.buildx-cache
          # Key is named differently to avoid collision
          key: ${{ runner.os }}-multi-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-multi-buildx

      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          context: .
          push: true
          tags: |
            cloud.canister.io:5000/bullyrooks/message-generator:${{ env.VERSION }}
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
        # This ugly bit is necessary if you don't want your cache to grow forever
        # till it hits GitHub's limit of 5GB.
        # Temp fix
        # https://github.com/docker/build-push-action/issues/252
        # https://github.com/moby/buildkit/issues/1896
      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

      - name: Publish Helm chart
        uses: stefanprodan/helm-gh-pages@master
        with:
          token: ${{ secrets.CHART_TOKEN }}
          charts_dir: helm
          charts_url: https://bullyrooks.github.io/helm-charts/
          repository: helm-charts
          branch: gh-charts
          app_version: ${{ env.VERSION }}
          chart_version: ${{ env.VERSION }}

      - name: Deploy
        uses: WyriHaximus/github-action-helm3@v2
        with:
          exec: |
            helm repo add bullyrooks https://bullyrooks.github.io/helm-charts/
            helm repo update
            echo "helm upgrade message-generator bullyrooks/message-generator --install --version ${{ env.VERSION }}"
            sleep 60s
            helm repo update
            helm upgrade message-generator bullyrooks/message-generator --install --version ${{ env.VERSION }}
          kubeconfig: '${{ secrets.KUBECONFIG }}'

      - name: Create Pact Release Record
        run: |
          docker run --rm pactfoundation/pact-cli:latest \
          broker record-release \
          --environment=okteto \
          --pacticipant=message-generator \
          --version=${{ env.VERSION }} \
          --broker-base-url=${{ env.PACTFLOW_URL }} \
          --broker-token=${{ env.PACTFLOW_TOKEN }}

      - name: Create Pact Deployment Record
        run: |
          docker run --rm pactfoundation/pact-cli:latest \
          broker record-deployment \
          --environment=okteto \
          --pacticipant=message-generator \
          --version=${{ env.VERSION }} \
          --broker-base-url=${{ env.PACTFLOW_URL }} \
          --broker-token=${{ env.PACTFLOW_TOKEN }}

The first part is the same. However, I need the version when I run the verify step here, so I’ve moved the “Bump Version” task. This will actually run the tests and publish the results back to the pactflow service (pact.verifier.publishResults=true). I’m telling it which version and branch I am running against so that the verification results can be stamped correctly. Additionally, I’m stamping with my environment (okteto), so that can know “where” my contract verification is valid.

The next part is the ‘can-i-deploy’ check, which comes before the Deploy task. This checks to see if the version that I’m about to deploy is compatible with all versions in my okteto environment. This can get really tricky with rollback, so if that is important to you make sure you have a rollback plan!

Finally, if everything works, I update the release and deploy record.

Build and Deploy

First, we need to create an ‘environment’ so that pact knows what services need to interact with each other in each environment. Lets make one for okteto

docker run --rm pactfoundation/pact-cli:latest \
broker \
create-environment \
--name=okteto \
--broker-base-url=https://bullyrooks.pactflow.io \
--broker-token=<your pactflow token>

Go ahead and commit your feature branch and lets go ahead and create a PR similar to what we did for the consumer. It should pass once the build succeeds

and when that succeeds, go ahead and merge to main. Watch this kick off. You should see your contract get verified and deployed successfully.

Also, you’ll see that the contract has not been fulfilled in pactflow

So we need to get the consumer rebuilt now that we’ve got a provider in place.

0 comments on “Kube Cloud Pt6 | Provider Contract Test for REST EndpointsAdd yours →

Leave a Reply

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