Pact Provider Test Example Java
How to write and validate Pact contracts using JUnit5 and REST Assured
What I've learned so far.
I remember working an as Android developer and facing some few issues where the UI from our app would break due to our backend not respecting the format that we were expecting for some of the data fields. For example, where the front end was expecting an integer we received a boolean value, or an id
changed from being a String to a long. I also remember wishing we could avoid this from happening but it was only after I joined HMH as an automation engineer that I learned of the existence of Pact tests.
After playing around with it for a few months now and having learned some things, I've decided to write this post in case it may be helpful to someone else who is also starting to go down this route. I'll assume you know what Pact is and what it does, so we can go straight to the how to part. I'll be using Java, REST Assured, Maven and JUnit5, so it may be good if you're familiar with those, but if not, of course you're welcome to keep reading if anything may be useful to your specific situation. Don't worry, we are all friends here. :)
First thing we'll do is add some dependencies to the POM file.
<json-schema-validator.version>3.3.0</json-schema-validator.version>
<junit.jupiter.version>5.5.2</junit.jupiter.version>
<pact.version>4.0.10</pact.version>
<maven.surefire.version>3.0.0-M5</maven.surefire.version> <dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-consumer-junit5</artifactId>
<version>${pact.version}</version>
</dependency>
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-provider-junit</artifactId>
<version>${pact.version}</version>
</dependency>
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-provider-junit5</artifactId>
<version>${pact.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>${json-schema-validator.version}</version>
<scope>test</scope>
</dependency>
We'll also add the below plugins. We've added the path to the pact broker that is hosted somewhere in our infrastructure and also to the location where the Pact contracts are generated (autogenerated target/pacts
folder). If you're just trying to start and run your tests locally having a broker set up may not be needed just yet and could be skipped for the moment.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
</plugin>
<plugin>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-provider-maven</artifactId>
<version>${pact.version}</version>
<configuration>
<pactDirectory>target/pacts</pactDirectory>
<pactBrokerUrl>http://pact-broker-your-domain.internal/</pactBrokerUrl>
<projectVersion>${project.version}</projectVersion>
<trimSnapshot>true</trimSnapshot>
</configuration>
</plugin>
As in our requests we need to authenticate the user to get a successful response, we'll be using REST Assured to call our authorize api and retrieve the SIF token from there to be used under the authorization header for that request. If your api doesn't require authorization, then it may be okay for you to skip this step too.
public static String getAuthorizationToken() {String url = https://your_domain.com/auth_login;
String username = "my_username";
String password = "my_password";Map<String, Object> map = new HashMap<>();
map.put("username", username);
map.put("password", password);Requests request = new Requests();
RequestSpecification rq = request.getRequestSpecification("");
Response r = rq.body(map).post(SUS_LOGIN);String authToken =r.getBody().jsonPath().get("sifToken");
return authToken;
}}
public class Requests {public RequestSpecification getRequestSpecification(String authorizationToken) {
RequestSpecification rq = given()
.contentType(OAuth.ContentType.JSON)
.contentType("application/json\r\n")
.header("Accept", "application/json").and()
.header("Content-Type", "application/json")
.header("Authorization", "authorizationToken")
.when()
.log()
.everything();return rq;
}
}
Now we should be ready to start writing our tests.
The first case is a POST call that requires this body of params:
{
"title":"My title",
"start_time":"2020–10–10T13:00:00Z",
"duration":30,
"provider":"MY_PROVIDER"
}
And responds with the following:
{
"topic":"My title",
"id":"94735196626",
"start_url":"https://myurl.com/",
"start_time":"2020–10–10T13:00:00Z",
"duration":30
}
This is how we set that up:
First, we need to annotate our class with the@ExtendWith(PactConsumerTestExt.class)
annotation.
We also need to match the path for the url, which is done by this piece String createMeeting = "/manage/create-meeting";
Consumer and providers are matched by their names @Pact(provider = "MY_PROVIDER", consumer = "MY_CONSUMER")
We set the headers for the response to make sure we're getting the format we expect (json, in our case). We do this by using the DSL format, so to get this response:
{
"title":"My title",
"start_time":"2020–10–10T13:00:00Z",
"duration":30,
"provider":"MY_PROVIDER"
}
We would use this DSL format:
new PactDslJsonBody()
.stringType("title", "My title")
.date("start_time", "yyyy-MM-dd'T'HH:mm:ss'Z'")
.numberType("duration", 30)
.stringType("provider", "MY_PROVIDER"))
We could actually have used the json blob as it is without turning it DSL, but to make sure we verify the fields properly when we are asserting our response, we go with the DSL to make use of its matchers, so we're using DSL here too to keep consistency across the whole class.
And speaking of which, this is the piece that validates the format for the fields:
DslPart bodyReceivedCreateMeeting = new PactDslJsonBody()
.stringType("topic", "My title")
.stringType("id", "94735196626")
.stringType("start_url", "https://myurl.com/")
.date("start_time", "yyyy-MM-dd'T'HH:mm:ss'Z'", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").parse(nowAsISO))
.integerType("duration", 30);
Notice here we are asserting that the field called "topic" is of String type, but it does not necessarily needs to be called "My title", which is just an example of a value that field may contain. If wanting to assert the exact value within the key we would be using .stringValue("topic", "My title")
instead.
For "start_time", which should be a date, we use .date
giving the date format we should be asserting against and for duration, which is an integer, we use .integer
instead. There are many other matches available, if needed, such as .booleanType
, .numberType
(for floats and doubles), etc.
Okay, for the above steps we defined what our contract should look like. We now only need to get the REST Assured in place to make sure we are able to receive the calls from the provider and verify the contract.
This is done here:
@Test
@PactTestFor(providerName = "MY_PROVIDER", port = "8080")
public void runTest() {//Mock url
RestAssured.baseURI = "http://localhost:8080";
RequestSpecification rq = RestAssured
.given()
.headers(headers)
.when();Map<String, Object> map = new HashMap<>();
map.put("title", "MyTitle");
map.put("start_time", nowAsISO);
map.put("provider", "MY_PROVIDER");
map.put("duration", 30);Response response = rq.body(map).post(createMeeting);
assert (response.getStatusCode() == 200);
}
You may not need this, but for our api we need our date to be dynamic so it won't expire. For this reason, we are passing a variable called "nowAsIso" that would be called at the beginning of our class.
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
String nowAsISO = df.format(new Date());
Okay, hope you're not too tired now, as you should be overjoyed since we've finally got our first Pact contract done. Yeahh! Well done you. Super proud.
@ExtendWith(PactConsumerTestExt.class)
public class PACTConsumerTest {
Map<String, String> headers = new HashMap<>();String createMeeting = "/manage/create-meeting";
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
String nowAsISO = df.format(new Date());
@Pact(provider = "MY_PROVIDER", consumer = "MY_CONSUMER")
public RequestResponsePact createPact(PactDslWithProvider builder) throws ParseException { headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");DslPart bodySentCreateMeeting = new PactDslJsonBody()
DslPart bodyReceivedCreateMeeting = new PactDslJsonBody()
.stringType("title", "My title")
.date("start_time", "yyyy-MM-dd'T'HH:mm:ss'Z'")
.stringType("provider", "MY_PROVIDER")
.numberType("duration", 30);
.stringType("topic", "My title")
.stringType("id", "94735196626")
.stringType("start_url", "https://my_url.com")
.date("start_time", "yyyy-MM-dd'T'HH:mm:ss'Z'", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").parse(nowAsISO))
.numberType("duration", 30);
return builder
.given("A request to create a meeting for my provider")
.uponReceiving("A request to create a meeting for my provider")
.path(createMeeting)
.method("POST")
.headers(headers)
.body(bodySentCreateMeeting)
.willRespondWith()
.body(bodyReceivedCreateMeeting)
.toPact();
}@Test
@PactTestFor(providerName = "MY_PROVIDER", port = "8080")
public void runTest() {//Mock url
RestAssured.baseURI = "http://localhost:8080";
RequestSpecification rq = RestAssured
.given()
.headers(headers)
.when();Map<String, Object> map = new HashMap<>();
map.put("title", "MyTitle");
map.put("start_time", nowAsISO);
map.put("provider", "MY_PROVIDER");
map.put("duration", 30);Response response = rq.body(map).post(createMeeting);
assert (response.getStatusCode() == 200);
}
}
The above is the final complete piece and if you run the class you should get a new target/pacts
folder that contains this contract.
Okay, wait. We're not totally done yet. I know I said we were and we have indeed created our contract, however we haven't verified whether our API respects it yet! But don't worry, this part is usually shorter and easier. Here is the full blob.
@Provider("MY_PROVIDER")
/** Uncomment this and comment @PactBroker instead to test locally by pasting a .json file for the contract under
the target/pacts folder */
//@PactFolder("target/pacts")
@PactBroker(host = BROKER_PACT_URL, consumers = {"MY_CONSUMER"})
public class PactProviderTest { @BeforeEach
void before(PactVerificationContext context) {
context.setTarget(new HttpsTestTarget("our-url.without.http-piece.internal", 443, "/"));getAuthorizationToken();
@TestTemplate
}
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactTestTemplate(PactVerificationContext context, HttpRequest request) {
request.addHeader("Authorization", AUTHORIZATION_TOKEN);
context.verifyInteraction();
}@State("A request to create a meeting for my provider")
public void sampleState() {
}}
Again we match provider against consumer names.
We call our REST Assured getAuthorizationToken at the start of the test so we can authenticate the user with their new SIF token. It sets a constant we have called AUTHORIZATION_TOKEN which is used for setting the SIF token we retrieved from our authorize api as one of the header params for our other calls. If you don't need to authorize your user this way you may want to skip this step.
But wait, there's an empty method annotated with @State
. Have we forgotten to implement it? No, no. This is expected. It will get the test it should verify against based on the A request to create a meeting for my provider
piece, which should be a unique identifier within that contract.
If you notice it, this identifier is the same as we had when building the contract. :)
return builder
.given("A request to create a meeting for my provider")
.uponReceiving("A request to create a meeting for my provider")
.path(createMeeting)
.method("POST")
.headers(headers)
.body(bodySentCreateMeeting)
.willRespondWith()
.body(bodyReceivedCreateMeeting)
.toPact();
As a contract may have different states and as an extra scenario, let's say that based on which params you're giving you'd be verifying against different providers. In that case we'd have something like this:
@State("A request to create a meeting for my provider")
public void sampleState() {
} @State("A request to create a meeting for my other provider")
public void sampleState1() {
}
And now if we run this provider class we'll be verifying against what is in the target/pacts
folder. If our contract is already published to the broker we can use the broker url instead (there's a comment on the top of our provider class explaining this bit).
And this is it for our first case.
Should you feel curious about what would we have done differently if testing for a GET call rather than POST (our second case), we would be changing it just a bit, and this small gist got from another request may be useful to clarify that.
.given("A request to retrieve a meeting for my provider")
.uponReceiving("A request to retrieve a meeting for my provider")
.path(getMeeting)
.method("GET")
.headers(headers)
.willRespondWith()
.body(bodyMeeting)
The way we would code for our contract wouldn't change much other than adapting it for the fields that we are verifying against, but we wouldn't need a body with params and our method piece would explicitly say "GET" rather than "POST".
And we are done.
I know Pact can feel a bit not too intuitive at the beginning and require a learning curve to be bypassed but once you write a few contracts and verifications it becomes quite easy and a very helpful and interesting tool.
I hope you will find this article useful and if you'd like some more examples you can check this other article.
Thank you for reading and have a good one.
Source: https://hmh.engineering/how-to-write-and-validate-pact-contracts-using-junit5-and-restassured-72b578e7dd65
0 Response to "Pact Provider Test Example Java"
Post a Comment