DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
  1. DZone
  2. Data Engineering
  3. Data
  4. Keep Your Promises: Contract-Based Testing for JAX-RS APIs

Keep Your Promises: Contract-Based Testing for JAX-RS APIs

In this article, Andriy Redko explains the simple steps that are required to implement contract-based testing for your APIs.

Andriy Redko user avatar by
Andriy Redko
·
Nov. 29, 16 · Tutorial
Like (2)
Save
Tweet
Share
8.91K Views

Join the DZone community and get the full member experience.

Join For Free

it's been a while since we talked about testing and applying effective tdd practices, particularly related to restful web services and apis. however, this topic should have never been forgotten, especially in the world where everyone is doing microservices whatever it means, implies, or takes.

to be fair, there are quite a lot of areas where microservice-based architecture shines and allows organizations to move and innovate much faster. however, without a proper discipline, it also makes our systems fragile, as they become very loosely coupled. in today's post, we are going to talk about contract-based testing and consumer-driven contracts as a practical and reliable techniques to ensure that our microservices fulfill their promises.

so, how does contract-based testing work? in nutshell, it is surprisingly simple technique and is guided by following steps:

  • the provider (let's say service a) publishes its contact (or specification); the implementation may not even be available at this stage.
  • the consumer (let's say service b) follows this contract (or specification) to implement conversations with service a.
  • additionally, the consumer introduces a test suite to verify its expectations regarding service a contract fulfillment.

in the case of soap web services and apis, things are obvious as there is an explicit contract in the form of the wsdl file. however, in the case of restful apis, there are a lot of different options around the corner ( wadl , raml , swagger , etc.) and still no agreement on one. it may sound complicated, but please don't get upset, because pact is coming to the rescue!

pact is family of frameworks for supporting consumer-driven contracts testing. there are many language bindings and implementations available, including jvm ones, jvm pact and scala-pact . to evolve such a polyglot ecosystem, pact also includes a dedicated specification so to provide interoperability between different implementations.

great! pact is there, the stage is set, and we are ready to take off with some real code snippets. let's assume we are developing a restful web api for managing people, using terrific apache cxf and jax-rs 2.0 specification. to keep things simple, we are going to introduce only two endpoints:

  • post /people/v1 to create a new person.
  • get /people/v1?email=<email> to find the person by email address.
essentially, we may not bother and just communicate these minimal pieces of our service contract to everyone so let the consumers deal with that themselves (and indeed, pact supports such a scenario). surely, we are not like that; we do care and would like to document our apis comprehensively. likely, we are already familiar with swagger . with that, here is our peoplerestservice :
@api(value = "manage people")
@path("/people/v1")
@consumes(mediatype.application_json)
@produces(mediatype.application_json)
public class peoplerestservice {
    @get
    @apioperation(value = "find person by e-mail", 
        notes = "find person by e-mail", response = person.class)
    @apiresponses({
        @apiresponse(code = 404, 
            message = "person with such e-mail doesn't exists", 
            response = genericerror.class)
    })
    public response findperson(
        @apiparam(value = "e-mail address to lookup for", required = true) 
        @queryparam("email") final string email) {
        // implementation here
    }

    @post
    @apioperation(value = "create new person", 
        notes = "create new person", response = person.class)
    @apiresponses({
        @apiresponse(code = 201, 
            message = "person created successfully", 
            response = person.class),
        @apiresponse(code = 409, 
            message = "person with such e-mail already exists", 
            response = genericerror.class)
    })
    public response addperson(@context uriinfo uriinfo, 
        @apiparam(required = true) personupdate person) {
        // implementation here
    }
}

the implementation details are not important at the moment. however, let's take a look at the genericerror , personupdate , and person classes, as they are an integral part of our service contract.

@apimodel(description = "generic error representation")
public class genericerror {
    @apimodelproperty(value = "error message", required = true)
    private string message;
}

@apimodel(description = "person resource representation")
public class personupdate {
    @apimodelproperty(value = "person's first name", required = true) 
    private string email;
    @apimodelproperty(value = "person's e-mail address", required = true) 
    private string firstname;
    @apimodelproperty(value = "person's last name", required = true) 
    private string lastname;
    @apimodelproperty(value = "person's age", required = true) 
    private int age;
}

@apimodel(description = "person resource representation")
public class person extends personupdate {
    @apimodelproperty(value = "person's identifier", required = true) 
    private string id;
}

excellent! once we have swagger annotations in place and apache cxf swagger integration turned on, we could generate swagger.json specification file, bring it to live in swagger ui and distribute to every partner or interested consumer.

image title

image title

it would be great if we could use this swagger specification along with pact framework implementation to serve as a service contract. thanks to atlassian , we are certainly able to do that using swagger-request-validator , a library for validating http requests and responses against a swagger and openapi specification that nicely integrates with pact jvm, as well.

cool! now, let's switch sides from provider to consumer and try to figure out what we can do having such swagger specification in our hands. it turns out, we can do a lot of things. for example, let's take a look at the post action, which creates a new person. as a client (or consumer), we could express our expectations in such a form that having a valid payload submitted along with the request, we would expect http status code 201 to be returned by the provider and the response payload should contain a new person with an identifier assigned. in fact, translating this statement into pact jvm assertions is pretty straightforward.

@pact(provider = provider_id, consumer = consumer_id)
public pactfragment addperson(pactdslwithprovider builder) {
    return builder
        .uponreceiving("post new person")
        .method("post")
        .path("/services/people/v1")
        .body(
            new pactdsljsonbody()
                .stringtype("email")
                .stringtype("firstname")
                .stringtype("lastname")
                .numbertype("age")
        )
        .willrespondwith()
        .status(201)
        .matchheader(httpheaders.content_type, mediatype.application_json)
        .body(
            new pactdsljsonbody()
                .uuid("id")
                .stringtype("email")
                .stringtype("firstname")
                .stringtype("lastname")
                .numbertype("age")
        )
       .tofragment();
}

to trigger the contract verification process, we are going to use awesome junit and very popular rest assured framework. before that, let's clarify what provider_id and consumer_id are from the code snippet above. as you may expect, provider_id is the reference to the contract specification. for simplicity, we would fetch swagger specification from running peoplerestservice endpoint. luckily, spring boot testing improvements make this task a no-brainer.

@runwith(springjunit4classrunner.class)
@springboottest(webenvironment = webenvironment.random_port, 
    classes = peoplerestconfiguration.class)
public class peoplerestcontracttest {
    private static final string provider_id = "people rest service";
    private static final string consumer_id = "people rest service consumer";

    private validatedpactproviderrule provider;
    @value("${local.server.port}")
    private int port;

    @rule
    public validatedpactproviderrule getvalidatedpactproviderrule() {
        if (provider == null) {
            provider = new validatedpactproviderrule("http://localhost:" + port + 
                "/services/swagger.json", null, provider_id, this);
        }

        return provider;
    }
}

the consumer_id is just a way to identify the consumer, not much to say about it. with that, we are ready to finish up with our first test case:

@test
@pactverification(value = provider_id, fragment = "addperson")
public void testaddperson() {
    given()
        .contenttype(contenttype.json)
        .body(new personupdate("tom@smith.com", "tom", "smith", 60))
        .post(provider.getconfig().url() + "/services/people/v1");
}

awesome! as simple as that. please notice the presence of the @pactverification annotation where we are referencing the appropriate verification fragment by name. in this case, it points out the addperson method that we have introduced before.

great, but...what's the point? glad you are asking, because from now on, any change in the contract which may not be backward compatible will break our test case. for example, if the provider decides to remove the id property from the response payload, the test case will fail. renaming the request payload properties is big no-no; again, the test case will fail. adding new path parameters? no luck; the test case won't let it pass. you may go even further than that and fail on every contract change, even if it's backward-compatible (using swagger-validator.properties for fine-tuning).

validation.response=error
validation.response.body.missing=error

not a very good idea but still, if you need it, it is there. similarly, let us add a couple of more test cases for the get endpoint, starting from a successful scenario where the person we are looking for exists. for example:

@pact(provider = provider_id, consumer = consumer_id)
public pactfragment findperson(pactdslwithprovider builder) {
    return builder
        .uponreceiving("get find person")
        .method("get")
        .path("/services/people/v1")
        .query("email=tom@smith.com")
        .willrespondwith()
        .status(200)
        .matchheader(httpheaders.content_type, mediatype.application_json)
        .body(
            new pactdsljsonbody()
                .uuid("id")
                .stringtype("email")
                .stringtype("firstname")
                .stringtype("lastname")
                .numbertype("age")
        )
        .tofragment();
}

@test
@pactverification(value = provider_id, fragment = "findperson")
public void testfindperson() {
    given()
        .contenttype(contenttype.json)
        .queryparam("email", "tom@smith.com")
        .get(provider.getconfig().url() + "/services/people/v1");
}

please take a note that here we introduced query string verification using query("email=tom@smith.com") assertion. following the possible outcomes, let us also cover the unsuccessful scenario, where person does not exist and we expect some error to be returned, along with the 404 status code. for example:

@pact(provider = provider_id, consumer = consumer_id)
public pactfragment findnonexistingperson(pactdslwithprovider builder) {
    return builder
        .uponreceiving("get find non-existing person")
        .method("get")
        .path("/services/people/v1")
        .query("email=tom@smith.com")
        .willrespondwith()
        .status(404)
        .matchheader(httpheaders.content_type, mediatype.application_json)
        .body(new pactdsljsonbody().stringtype("message"))
        .tofragment();
}

@test
@pactverification(value = provider_id, fragment = "findnonexistingperson")
public void testfindpersonwhichdoesnotexist() {
    given()
        .contenttype(contenttype.json)
        .queryparam("email", "tom@smith.com")
        .get(provider.getconfig().url() + "/services/people/v1");
}

this is a really brilliant, maintainable, understandable, and non-intrusive approach to address such complex and important problems as contract-based testing and consumer-driven contracts . hopefully, this somewhat new testing technique would help you to catch more issues during the development phase way before they would have a chance to leak into production.

thanks to swagger, we were able to take a few shortcuts, but in case you don't have such a luxury, pact has a quite rich specification that you are very welcome to learn and use. in any case, pact jvm does a really great job in helping you out writing small and concise test cases.

the complete project sources are available on github .

PACT (interaction design) microservice Web Service Test case Testing

Published at DZone with permission of Andriy Redko, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • 3 Main Pillars in ReactJS
  • Getting a Private SSL Certificate Free of Cost
  • AWS CodeCommit and GitKraken Basics: Essential Skills for Every Developer
  • Accelerating Enterprise Software Delivery Through Automated Release Processes in Scaled Agile Framework (SAFe)

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: