Kube Cloud Pt6 | Contract Testing
full course- Kube Cloud Pt6 | Contract Testing
- Kube Cloud Pt6 | Consumer Contract Tests for REST Endpoints
- Kube Cloud Pt6 | Provider Contract Test for REST Endpoints
- Kube Cloud Pt6 | Fulfill the Consumer Contract Test for REST Endpoint
- Kube Cloud Pt6 | Break the Contract from a Consumer Change
- 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 Endpoints”Add yours →