Consumer-driven contract (CDC) tests are a technique to test integration points between API providers and API consumers without the hassle of end-to-end tests (read it up in a recent blog post). A common use case for consumer-driven contract tests is testing interfaces between services in a microservice architecture.
This article explains the steps of setting up a GraphQL client (or “consumer”) using the Apollo framework. We’ll then create and publish a consumer-driven contract for the GraphQL interaction between the GraphQL client and the API provider and implement a contract test that validates that our consumer is working as expected by the contract. For this, we’re using the Node version of the Pact framework.
This tutorial builds upon a recent tutorial about creating a React consumer for a REST API, so you’ll find some links to that tutorial for more detailed explanations.
Example Code
This article is accompanied by a working code example on GitHub.Creating the Node App
To set up a Node app, follow the instructions in the previous tutorial.
There, we’re using the create-react-app
tool to create a React client that already has Jest set up as
a testing framework.
However, since we’re not using React in this tutorial, you can also create a plain Node app. Then you have to set up a test framework manually, though.
Adding Dependencies
In our package.json
, we need to declare some additional dependencies:
{
"dependencies": {
"apollo-cache-inmemory": "^1.3.9",
"apollo-client": "^2.4.5",
"apollo-link-http": "^1.5.5",
"graphql": "^14.0.2",
"graphql-tag": "^2.10.0",
"node-fetch": "^2.2.1"
}
}
- apollo-client provides Apollo’s GraphQL client implementation
- apollo-cache-inmemory contains Apollo’s implementation of an in-memory-cache that is used to cache GraphQL query results to reduce the number of requests to the server
- apollo-link-http allows us to use GraphQL over HTTP
- graphql and graphql-tag provide the means to work with GraphQL queries
- node-fetch implements the global
fetch
operation that is available in browsers, but not in a Node environment.
Don’t forget to run npm install
after changing the dependencies.
Setting Up Jest
We’re using Jest as the testing framework for our contract tests.
Follow the instructions in the previous tutorial to set up Jest. If you want the code of the previous tutorial and the code of this tutorial to exist in parallel, note these changes:
- copy the file
pact/setup.js
topact/setup-graphql.js
and use different consumer and provider names - in
package.json
add a scripttest:pact:graphql
referring topact/setup-graphql.js
and using--testMatch \"**/*.test.graphql.pact.js\"
in order to only execute our graphQL client tests
Now, we can run the pact tests with this command:
npm run test:pact:graphql
We just don’t have a test to run, yet.
The Hero GraphQL Client
Let’s implement a GraphQL client that we can test.
We’re going to create a client that allows us to query heroes from a GraphQL server.
The Hero Class
A hero resource has an id, a name, a superpower and it belongs to a certain universe (e.g. “DC” or “Marvel”):
// hero.js
class Hero {
constructor(name, superpower, universe, id) {
this.name = name;
this.superpower = superpower;
this.universe = universe;
this.id = id;
}
}
export default Hero;
Strictly, we don’t need to declare a class for our hero objects, since we can just use plain JSON objects instead. However, having a Java background, I couldn’t resist the urge to fake type safety ;).
The Hero GraphQL Client Service
For loading a hero from the server via GraphQL, we’re creating the GraphQLHeroService
class:
import {ApolloClient} from "apollo-client"
import {InMemoryCache} from "apollo-cache-inmemory"
import {HttpLink} from "apollo-link-http"
import gql from "graphql-tag"
import Hero from "hero";
class GraphQLHeroService {
constructor(baseUrl, port, fetch) {
this.client = new ApolloClient({
link: new HttpLink({
uri: `${baseUrl}:${port}/graphql`,
fetch: fetch
}),
cache: new InMemoryCache()
});
}
getHero(heroId) {
if (heroId == null) {
throw new Error("heroId must not be null!");
}
return this.client.query({
query: gql`
query GetHero($heroId: Int!) {
hero(id: $heroId) {
name
superpower
}
}
`,
variables: {
heroId: heroId
}
}).then((response) => {
return new Promise((resolve, reject) => {
try {
const hero = new Hero(response.data.hero.name,
response.data.hero.superpower,
null,
heroId);
Hero.validateName(hero);
Hero.validateSuperpower(hero);
resolve(hero);
} catch (error) {
reject(error);
}
})
});
};
}
export default GraphQLHeroService;
First, we’re creating a new ApolloClient
that is pointed to a certain URL and port.
In the constructor, we’re passing a fetch
function. In a browser environment, this is a globally
available function. However, we’re going to run our tests in a Node environment where this function
is not available by default. So, to make our service compatible to both environments, we’re taking
a fetch function as a parameter and pass it on to be used by the GraphQL client.
In the getHero
function, we’re using gql
to create a GraphQL query.
Implementing a Contract Test
In this test, we’re going to:
- create a contract between our GraphQL client and GraphQL provider
- verify that our GraphQL client works as defined in the contract.
The Test Template
The test structure will look like this:
// hero.service.test.graphql.pact.js
import GraphQLHeroService from './hero.service.graphql';
import * as Pact from '@pact-foundation/pact';
import fetch from 'node-fetch';
describe('HeroService GraphQL API', () => {
const heroService = new HeroService('http://localhost', global.port, fetch);
describe('getHero()', () => {
beforeEach((done) => {
// ...
});
it('sends a request according to contract', (done) => {
// ...
});
});
});
We see the usual describe()
and it()
functions popular in javascript testing frameworks.
Also, we create an instance of our GraphQLHeroService
GraphQL client and tell it to please send its
requests to localhost:8080
.
Additionally, we’re importing the fetch
function from node-fetch
to pass it into our GraphQLHeroService
to
make it compatible within the Node environment.
We’ll fill in the beforeEach()
and it()
functions next.
Defining the Contract
Within the beforeEach
function, we’re defining our contract:
// hero.service.test.graphql.pact.js
beforeEach((done) => {
const contentTypeJsonMatcher = Pact.Matchers.term({
matcher: "application\\/json; *charset=utf-8",
generate: "application/json; charset=utf-8"
});
global.provider.addInteraction(new Pact.GraphQLInteraction()
.uponReceiving('a GetHero Query')
.withRequest({
path: '/graphql',
method: 'POST',
})
.withOperation("GetHero")
.withQuery(`
query GetHero($heroId: Int!) {
hero(id: $heroId) {
name
superpower
__typename
}
}`)
.withVariables({
heroId: 42
})
.willRespondWith({
status: 200,
headers: {
'Content-Type': contentTypeJsonMatcher
},
body: {
data: {
hero: {
name: Pact.Matchers.somethingLike('Superman'),
superpower: Pact.Matchers.somethingLike('Flying'),
__typename: 'Hero'
}
}
}
}))
.then(() => done());
});
By calling provider.addInteraction()
, we’re passing a request / response pair to the
pact mock server (which has been started by the jest-wrapper.js
script we defined above).
Since we want to create a GraphQL interaction, we’re using Pact’s GraphQLInteraction
class
to describe this interaction.
The differences to a standard REST interaction are the .withOperation()
, .withQuery()
and
.withVariables()
functions. These we can use to define the name of the GraphQL operation (if
we have defined a name in the query), the GraphQL query itself and the variables used within the query.
For a discussion of the GraphQL Syntax, refer to the GraphQL documentation.
Note the __typename
field in the query. We have not defined such a field in our Hero
class.
However, the Apollo GraphQL client adds this field by itself, so we need to include it into our
contract.
Also note that whitespaces are not important in the GraphQL query. If the GraphQL client adds whitespaces and line breaks in a different manner, it doesn’t matter.
Verifying the GraphQL Client
Now, we want to make sure that our GraphQLHeroService
works as expected by the contract.
We do this in the actual test method it()
:
// hero.service.test.graphql.pact.js
it('sends a request according to contract', (done) => {
heroService.getHero(42)
.then(hero => {
expect(hero.name).toEqual('Superman');
})
.then(() => {
global.provider.verify()
.then(() => done(), error => {
done.fail(error)
})
});
});
We’re calling our heroService
to fetch a hero for us. Since the heroService
is configured to send requests to the Pact mock provider, Pact can check
if the request matches a certain request / response pair.
In our case, we have only defined a single request / response pair, so if the
request does not match the request we have defined in our before()
function above,
we’ll get an error.
If the request matches, the Pact mock provider will return the response we have
provided in the contract. To prove that, we assert that the heroes name
is the
one we provided in the contract.
By calling provider.verify()
we also make sure
that the test fails if the heroService
doesn’t send any request at all or a request that
did not match any of the registered interactions.
We can now run our test with npm run test:pact:graphql
and it should be green. Also, it should
have created a contract file in the pacts
folder that can be published so that the provider
can test against it, too.
Improving Contract Quality with Validation
Read this discussion in my previous tutorial.
Debugging
Read this discussion in my previous tutorial.
Publishing the Contract
Read this discussion in my previous tutorial.
Conclusion
In this tutorial, we have successfully created a GraphQL client with Node and Apollo. We have also defined a contract for this client and verified that this client works as expected by the contract.
The contract can now be used to verify that a certain GraphQL provider works as expected.
The code for this tutorial can be found on github.