Contract Tests vs Integration Tests
- Trustworthy like the API tests, even though the contract test is mocking the provider/consumer, you know it is mocking based on the contract that was generated.
- Reliable because you don’t depend on your internet connection to get the same consistency on the results (When your API does’t have third parties integration or you are testing locally).
- Fast because you don’t need internet connection, everything is mocked using the contract that was generated.
- Cheap because you don’t spend huge amount of time to create a pact test or to run it, even less to maintain.
Contract Tests | API Tests |
Trustworthy | Trustworthy |
Reliable | Not realiable |
Fast | Slow |
Cheap | Expensive |
Remember contract tests are NOT about testing the performance of your microservice. So, if you have API Test that are taking ages to (execute/perform), failing due server no replying fast enough or timeouts, this means you have a performance problem, or it is just your internet connection. In either case you need to separate the problem and create targeted tests that are going to verify the performance of your server and not the expected response body/code.
How it works
You can use a framework like Pact which will generate the contract details and fields from the consumer. You need to specify the data you are going to send and in the verification part you will use the same function the app would use to do the requests to the API.
Contract test is part of an integration test stage where you don’t really need to hit the API, so it is faster and reliable, completely independent of your internet connection. It is trustworthy since you are generating the contract based on the same function and the same way you would do when using the consumer to hit the provider. Pact is responsible to generate this contract for you, so you just need to worry about passing the data and add the assertions, like response code, headers, etc. It seems pretty straight forward to know who is the consumer, who is the provider and the contract that you are going to generate, but imagine a more complex real life scenario where you have a structure like:
In this case you have multiple microservices communicating with each other and sometimes this service is the provider and sometimes the same service is the consumer. So, to keep the house organised when maintaining these services you need to create a pact between each one of them.
The fun part
So let’s get hands-on now and see how we can actually create these contracts.
Create a helper for the consumer to setup and finalise the provider (this will be the pact mock where the consumer is going to point when creating the pact.)
import { Pact } from '@pact-foundation/pact' import path from 'path' jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000 export const provider = new Pact({ port: 20002, log: path.resolve(process.cwd(), 'logs', 'mockserver-integration.log'), dir: path.resolve(process.cwd(), 'pacts'), pactfileWriteMode: 'overwrite', consumer: 'GraphQLConsumer', provider: 'GraphQLProvider' }) beforeAll(() => provider.setup()) afterAll(() => provider.finalize()) // verify with Pact, and reset expectations afterEach(() => provider.verify())
Then create a consumer file where you add what is the data you want to check and the response you are expecting from the graphQL API.
import { Matchers, GraphQLInteraction } from '@pact-foundation/pact' import { addTypenameToDocument } from 'apollo-utilities' import gql from 'graphql-tag' import graphql from 'graphQLAPI' const { like } = Matchers const product = { id: like('456789').contents, disabled: false, type: like('shampoo').contents, } describe('GraphQL', () => { describe('query product list', () => { beforeEach(() => { const graphqlQuery = new GraphQLInteraction() .uponReceiving('a list of products') .withRequest({ path: '/graphql', method: 'POST' }) .withOperation('ProductsList') .withQuery(print(addTypenameToDocument(gql(productsList)))) .withVariables({}) .willRespondWith({ status: 200, headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: { data: { productsList: { items: [ product ], nextToken: null } } } }) return provider.addInteraction(graphqlQuery) }) it('returns the correct response', async () => { expect(await graphql.productsList()).toEqual([product]) }) }) })
When you run the script above, pact is going to create a .json file in your pacts folder and this will be used to test the provider side. So, this is going to be the source of truth for your tests.
This is the basic template if you are using jest, just set the timeout and then you need to use the same functions that you are going to use for the consumer to communicate with the provider. You just need to decide how you are going to inject the data in your local database, you can pre-generate all the data on the beforeAll or a pre-test and then add a post-test or a function in your afterAll to clean the database once the tests are done.
The provider.js file should be something similar to this one:
import { Verifier } from '@pact-foundation/pact' import path from 'path import server from 'server' jest.setTimeout(30000) beforeAll(async () => { server.start('local') }) afterAll(async () => { server.tearDown() }) describe('Contract Tests', () => { it('validates the pact is correct', () => { const config = { pactUrls: [path.resolve(process.cwd(), 'pacts/graphqlconsumer-graphqlprovider.json')], pactBrokerPassword: "Password", pactBrokerUrl: "https://test.pact.com/", pactBrokerUsername: "Username", provider:'GraphQLProvider', providerBaseUrl:server.getGraphQLUrl(), publishVerificationResult:true } return new Verifier(config).verifyProvider() } })
In the end you just need to verify that the contract is still valid after your changes on provider or consumer, for this reason you don’t need to add edge scenarios, just exactly what the provider is expecting as data.
Resources:
https://docs.pact.io/pact_broker/advanced_topics/how_pact_works
https://medium.com/humanitec-developers/testing-in-microservice-architectures-b302f584f98c
Hello Rafaela,
First of all thank you for the great article.
If you don’t mind, I tend to disagree a bit in some parts of it:
_”Reliable because you don’t depend on your internet connection to get the same consistency on the results.”_
Note that functional API tests may not rely on internet connection, and I would say that if it is not a third party API, normally they don’t rely at all. (only network connections, which normally are redundant and in the same DC or infrastructure). Also if it is a third party API, then is where I possibly apply the contract testing with full force :D.
_”Remember contract tests are to test the integration, NOT the performance of your microservice”_
I would say that they are to test that the contract between the consumer and the provider is the correctly specified. Ofcourse I agree they help to test the integration but only at some scale. Quoting Pact itself (https://docs.pact.io/best_practices/consumer/contract_tests_not_functional_tests):
” A contract test would ensure that the consumer and provider had a shared and accurate understanding of the request and response required to create an order”
“functional test for the provider would ensure that when a given request was made, that an Order with the correct attributes was actually persisted to the underlying datastore”
“A contract test does not check for side effects.”
Also contract testing should be more open and flexible, so validation rules, when existing should be left out of contract testing (see also the same article).
Again, thank you for the great article and the knowledge sharing.
Hello,
Yes not all of API tests need the internet connection, I think I should have specified E2E API Tests and thirs party integration(which is my case now), where you have the internet connection flakyness. Thanks, will update the post specifying this 🙂