Pact with Quarkus 3
This article will teach you how to write contract tests with Pact for the app built on top of version 3 of the Quarkus framework. It is an update to the previously described topic in the “Contract Testing with Quarkus and Pact” article. Therefore we will not focus on the details related to the integration between Pact and Quarkus, but rather on the migration from version 2 to 3 of the Quarkus framework. There are some issues worth discussing.
You can find several other articles about Quarkus on my blog. For example, you can read about advanced testing techniques with Quarkus here. There is also an interesting article about Quarkus the Testcontainer’s support in the local development with Kafka.
Source Code
If you would like to try it by yourself, you may always take a look at my source code. It contains three microservices written in Quarkus. I migrated them from Quarkus 2 to 3, to the latest version of the Pact Quarkus extension, and from Java 17 to 21. In order to proceed with the exercise, you need to clone my GitHub repository. Then you should just follow my instructions.
Let’s do a quick recap before proceeding. We are implementing several contact tests with Pact to verify interactions between our three microservices: employee-service
, department-service
, and organization-service
. We use Pact Broker to store and share contract definitions between the microservices. Here’s the diagram that illustrates the described architecture
Update to Java 21
There are no issues with migration to Java 21 in Quarkus. We need to change the version of Java used in the Maven compilation inside the pom.xml
file. However, the situation is more complicated with the CircleCI build. Firstly, we use the ubuntu-2204
machine in the builds to access the Docker daemon. We need Docker to run the container with the Pact broker. Although CircleCI provides the image for OpenJDK 21, there is still Java 17 installed on the latest version of ubuntu-2204. This situation will probably change during the next months. But now, we need to install OpenJDK 21 on that machine. After that, we may run Pact broker and JUnit tests using the latest Java LTS version. Here’s the CircleCI config.yaml
file:
version: 2.1
jobs:
analyze:
executor:
name: docker/machine
image: ubuntu-2204:2024.01.2
steps:
- checkout
- run:
name: Install OpenJDK 21
command: |
java -version
sudo apt-get update && sudo apt-get install openjdk-21-jdk
sudo update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java
sudo update-alternatives --set javac /usr/lib/jvm/java-21-openjdk-amd64/bin/javac
java -version
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
- docker/install-docker-compose
- maven/with_cache:
steps:
- run:
name: Build Images
command: mvn package -DskipTests -Dquarkus.container-image.build=true
- run:
name: Run Pact Broker
command: docker-compose up -d
- maven/with_cache:
steps:
- run:
name: Run Tests
command: mvn package pact:publish -Dquarkus.container-image.build=false
- maven/with_cache:
steps:
- run:
name: Sonar Analysis
command: mvn package sonar:sonar -DskipTests -Dquarkus.container-image.build=false
orbs:
maven: circleci/maven@1.4.1
docker: circleci/docker@2.6.0
workflows:
maven_test:
jobs:
- analyze:
context: SonarCloud
YAMLHere’s the root Maven pom.xml
. It declares the Maven plugin responsible for publishing contracts to the Pact broker. Each time the Pact JUnit is executed successfully, it tries to publish the JSON pacts to the broker. The ordering of Maven modules is not random. The organization-service
generates and publishes pacts for verifying contracts with department-service
and employee-service
, so it has to be run at the beginning. As you see, we use the current latest version of Quarkus – 3.9.3
.
<properties>
<java.version>21</java.version>
<surefire-plugin.version>3.2.5</surefire-plugin.version>
<quarkus.version>3.9.3</quarkus.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<modules>
<module>organization-service</module>
<module>department-service</module>
<module>employee-service</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>au.com.dius.pact.provider</groupId>
<artifactId>maven</artifactId>
<version>4.6.9</version>
<configuration>
<pactBrokerUrl>http://localhost:9292</pactBrokerUrl>
</configuration>
</plugin>
</plugins>
</build>
XMLHere’s the part of the docker-compose.yml
responsible for running a Pact broker. It requires a Postgres database.
version: "3.7"
services:
postgres:
container_name: postgres
image: postgres
environment:
POSTGRES_USER: pact
POSTGRES_PASSWORD: pact123
POSTGRES_DB: pact
ports:
- "5432"
pact-broker:
container_name: pact-broker
image: pactfoundation/pact-broker
ports:
- "9292:9292"
depends_on:
- postgres
links:
- postgres
environment:
PACT_BROKER_DATABASE_USERNAME: pact
PACT_BROKER_DATABASE_PASSWORD: pact123
PACT_BROKER_DATABASE_HOST: postgres
PACT_BROKER_DATABASE_NAME: pact
YAMLUpdate Quarkus and Pact
Dependencies
Firstly, let’s take a look at the list of dependencies. With the latest versions of Quarkus, we should take care of the REST provider and client used in our app. For example, if we use the quarkus-resteasy-jackson
module to expose REST services, we should also use the quarkus-resteasy-client-jackson
module to call the services. On the other hand, if we use quarkus-rest-jackson
on the server side, we should also use quarkus-rest-client-jackson
on the client side. In order to implement Pact tests in our app, we need to include the quarkus-pact-consumer
module for the contract consumer and the quarkus-pact-provider
on the contract provider side. Finally, we will use Wiremock to replace a Pact mock server.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-client-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkiverse.pact</groupId>
<artifactId>quarkus-pact-consumer</artifactId>
<version>1.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.quarkiverse.pact</groupId>
<artifactId>quarkus-pact-provider</artifactId>
<version>1.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>3.0.1</version>
<scope>test</scope>
</dependency>
XMLTests Implementation with Quarkus 3 and Pact Consumer
In that exercise, I’m simplifying tests as much as possible. Therefore we will use the REST client directly to verify the contract on the consumer side. However, if you are looking for more advanced examples please go to that repository. Coming back to our exercise, let’s take a look at the example of a declarative REST client used in the department-service
.
@ApplicationScoped
@Path("/employees")
@RegisterRestClient(configKey = "employee")
public interface EmployeeClient {
@GET
@Path("/department/{departmentId}")
@Produces(MediaType.APPLICATION_JSON)
List<Employee> findByDepartment(@PathParam("departmentId") Long departmentId);
}
JavaThere are some significant changes in the Pact tests on the consumer side. Some of them were forced by the error related to migration to Quarkus 3 described here. I found a smart workaround for that problem proposed by one of the contributors (1). This workaround replaces the Pact built-in mock server with Wiremock. We will start Wiremock on the dynamic port (2). We also need to implement @QuarkusTestResource
to start the Wiremock container before the tests and shut it down after the tests (3). Then, we can switch to the latest version of Pact API by returning the V4Pact
object (4) in the @Pact
method and updating the @PactTestFor
annotation accordingly (5). Finally, instead of the Pact MockServer
, we use the wrapper PactMockServer
dedicated to Wiremock (6).
@QuarkusTest
@ExtendWith(PactConsumerTestExt.class)
@ExtendWith(PactMockServerWorkaround.class) // (1)
@MockServerConfig(port = "0") // (2)
@QuarkusTestResource(WireMockQuarkusTestResource.class) // (3)
public class EmployeeClientContractTests extends PactConsumerTestBase {
@Pact(provider = "employee-service", consumer = "department-service")
public V4Pact callFindDepartment(PactDslWithProvider builder) { // (4)
DslPart body = PactDslJsonArray.arrayEachLike()
.integerType("id")
.stringType("name")
.stringType("position")
.numberType("age")
.closeObject();
return builder.given("findByDepartment")
.uponReceiving("findByDepartment")
.path("/employees/department/1")
.method("GET")
.willRespondWith()
.status(200)
.body(body).toPact(V4Pact.class);
}
@Test
// (5)
@PactTestFor(providerName = "employee-service", pactVersion = PactSpecVersion.V4)
public void verifyFindDepartmentPact(final PactMockServer mockServer) { // (6)
EmployeeClient client = RestClientBuilder.newBuilder()
.baseUri(URI.create(mockServer.getUrl()))
.build(EmployeeClient.class);
List<Employee> employees = client.findByDepartment(1L);
System.out.println(employees);
assertNotNull(employees);
assertTrue(employees.size() > 0);
assertNotNull(employees.get(0).getId());
}
}
JavaHere’s our PactMockServer
wrapper:
public class PactMockServer {
private final String url;
private final int port;
public PactMockServer(String url, int port) {
this.url = url;
this.port = port;
}
public String getUrl() {
return url;
}
public int getPort() {
return port;
}
}
JavaImplement Mock Server with Wiremock
In the first step, we need to provide an implementation of the QuarkusTestResourceLifecycleManager
for starting the Wiremock server during the tests.
public class WireMockQuarkusTestResource implements
QuarkusTestResourceLifecycleManager {
private static final Logger LOGGER = Logger
.getLogger(WireMockQuarkusTestResource.class);
private WireMockServer wireMockServer;
@Override
public Map<String, String> start() {
final HashMap<String, String> result = new HashMap<>();
this.wireMockServer = new WireMockServer(options()
.dynamicPort()
.notifier(createNotifier(true)));
this.wireMockServer.start();
return result;
}
@Override
public void stop() {
if (this.wireMockServer != null) {
this.wireMockServer.stop();
this.wireMockServer = null;
}
}
@Override
public void inject(final TestInjector testInjector) {
testInjector.injectIntoFields(wireMockServer,
new TestInjector.AnnotatedAndMatchesType(InjectWireMock.class,
WireMockServer.class));
}
private static Notifier createNotifier(final boolean verbose) {
final String prefix = "[WireMock] ";
return new Notifier() {
@Override
public void info(final String s) {
if (verbose) {
LOGGER.info(prefix + s);
}
}
@Override
public void error(final String s) {
LOGGER.warn(prefix + s);
}
@Override
public void error(final String s, final Throwable throwable) {
LOGGER.warn(prefix + s, throwable);
}
};
}
}
JavaLet’s create the annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectWireMock {
}
JavaI’m not very sure it is required. But here’s the test base extended by our tests.
public class PactConsumerTestBase {
@InjectWireMock
protected WireMockServer wiremock;
@BeforeEach
void initWiremockBeforeEach() {
wiremock.resetAll();
configureFor(new WireMock(this.wiremock));
}
protected void forwardToPactServer(final PactMockServer wrapper) {
wiremock.resetAll();
stubFor(any(anyUrl())
.atPriority(1)
.willReturn(aResponse().proxiedFrom(wrapper.getUrl()))
);
}
}
JavaHere’s the workaround implementation used as the test extension included with the @ExtendWith
annotation:
public class PactMockServerWorkaround implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext)
throws ParameterResolutionException {
return parameterContext.getParameter().getType() == PactMockServer.class;
}
@Override
@SuppressWarnings("unchecked")
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext)
throws ParameterResolutionException {
final ExtensionContext.Store store = extensionContext
.getStore(ExtensionContext.Namespace.create("pact-jvm"));
if (store.get("providers") == null) {
return null;
}
final List<Pair<ProviderInfo, List<String>>> providers = store
.get("providers", List.class);
var pair = providers.get(0);
final ProviderInfo providerInfo = pair.getFirst();
var mockServer = store.get("mockServer:" + providerInfo.getProviderName(),
MockServer.class);
return new PactMockServer(mockServer.getUrl(), mockServer.getPort());
}
}
JavaI intentionally do not comment on this workaround. Maybe it could be somehow improved. I wish that everything would work fine just after migrating the Pact extension to Quarkus 3 without any workarounds. However, thanks to the workaround, I was able to run my Pact tests successfully and then update all the required dependencies to the latest.
Final Thoughts
This article guides you through the changes required to migrate your microservices and Pact contract tests from Qaurkus 2 to 3. For me, it is important to automatically update all the dependencies in my demo projects to be up-to-date as described here. I’m using Renovate to automatically scan and update Maven pom.xml
dependencies. Once it updates the version of the dependency it runs all the JUnit tests for the verification. The process is automatically performed on the CircleCI. You can view the history of builds of the sample repository used in that article.
Leave a Reply