Transcripted Summary

Setting Up Pact

In this course we will use JavaScript for the programming language and Visual Studio Code for the IDE. As the focus of this course is contract testing, this lesson will use a scaffolding with a dummy API to save the trouble of creating an API from scratch.

After downloading the binary, the next step is to import the project into Visual Studio Code and set up Pact to run tests.

From the terminal, navigate to the directory where the project folder resides:


cd tau-pact-nodejs-course/

Within the project folder, execute the following command to install any package dependencies for this project:


npm i

Finally, install PACT with the following command:


npm install --save-dev @pact-foundation/pact

NOTE

While Node.js and JavaScript are used in this lesson, Pact supports several languages. You can use any language that Pact supports since the concepts are the same.


Returning to the project in Visual Studio, define the following folder structure. Create a folder called __tests__ and a folder inside called contract. Finally, create three folders inside of /contract/: consumer, helpers and provider. In this project we will use JavaScript to build the tests on top of a simple API.



The provider logic of this project is implemented in provider.js below. The provider is capable of making calls to the server as well as retrieving a collection of active clients using simple fixed endpoints. Clients can be searched by ID and basic validation is performed as well. Our tests will focus on these endpoints.


const express = require("express")
const cors = require("cors")
const bodyParser = require("body-parser")
const Repository = require("./repository")
const server = express()

server.use(cors())
server.use(bodyParser.json())
server.use(
 bodyParser.urlencoded({
   extended: true,
 })
)

server.use((req, res, next) => {
 res.header("Content-Type", "application/json; charset=utf-8")
 next()
})

const clientRepository = new Repository()

// Load client data into a repository object
const importData = () => {
 const data = require("./data/clientData.json")

 data.reduce((a, v) => {
   v.id = a + 1
   clientRepository.add(v)
   return a + 1
 }, 0)
}

// Get all clients
server.get("/clients", (req, res) => {
 res.json(clientRepository.fetchAll())
})

// Find client by ID
server.get("/clients/:id", (req, res) => {
 const response = clientRepository.getById(req.params.id)

 if (response) {
   res.end(JSON.stringify(response))
 } else {
   res.status(404)
   res.send({message: 'Client not found!'})
   res.end()
 }
})

// Add a new Client
server.post("/clients", (req, res) => {
 const client = req.body

 // Basic validation for missing first name field
 if (!client || !client.firstName) {
   res.status(400)
   res.send({message:'Missing first name!', body: req.body})
   res.end()
   return
 }
 client.id = clientRepository.fetchAll().length
 clientRepository.add(client)
 res.json(client)
})

module.exports = {
 server,
 importData,
 clientRepository,
}

Consumer logic is implemented in consumer.js below. This logic enables consumers to reach the provider. getClients stores the result of a function expression connecting to the endpoint on the server side while postClient is used to create the client.


const axios = require('axios')
const express = require("express")
const server = express()
const getApiEndpoint = "http://localhost:8081"
const getClients = async () => {
 const res = await axios
   .get(`${getApiEndpoint}/clients`)
   .then((res) => {
 	return res
   })
   .catch((err) => {
 	return err.res
   })
 return res
}

const getClient = async (id) => {
 	const res = await axios
   	.get(`${getApiEndpoint}/clients/${id}`)
   	.then((res) => {
     	return res;
   	})
   	.catch((err) => {
     	return err.res
   	})
   return res
}

const postClient = async (body) => {
 	const res = await axios
 	.post(`${getApiEndpoint}/clients`, body, {'Content-Type': 'application/json;charset=utf-8'})
 	.then((res) => {
     	return res
   	})
   	.catch((err) => {
     	return err.res
   	})
   return res
}

module.exports = {
 server,
 getClients,
 postClient,
 getClient,
};

The popularity of Pact is largely thanks to its support for service integration testing without the need to create mocks or stubs. Pact creates all of the infrastructure and reuses the same functions that the consumer uses to create contracts. Representative, real-world testing is possible while eliminating liabilities such as unreliable internet connectivity and mocking infrastructure maintenance. Pact integrates services by creating its contract and mocks around existing functions.


NOTE

Note that postClient requires you to pass the body which in this example consists of first name, last name, and age.


Client data is contained in clientData.json which serves as a data store. Retrieved and transmitted clients are expected to conform to this data format.


[
   {
   	"firstName": "Lisa",
   	"lastName": "Simpson",
   	"age": 8,
   	"id": 1
   },
   {
   	"firstName": "Wonder",
   	"lastName": "Woman",
   	"age": 30,
   	"id": 2
   },
   {
   	"firstName": "Homer",
   	"lastName": "Simpson",
   	"age": 39,
   	"id": 3
   }
]

Letā€™s pause here and run the provider service to verify that the API is working. From the terminal run the following command:


npm run provider



The client service will run on localhost:8081. Letā€™s switch over to Postman now to verify that requests are coming through.


GET Clients:[ http://localhost:8081/clients](http://localhost:8081/clients)
GET Client by ID: http://localhost:8081/clients/2

This collection can be imported into Postman for request verification. When the client's end points are hit the expected client data is displayed.



When we send the postClient we will include just the first name, last name and age. So this is the body we are sending:




Body: {"firstName":"Rafaela","lastName":"Azevedo","age":29}

And this is the expected response:



Note that the ID is generated by the server.

Back in the project we will now create our first helper. Inside the helpers folder, create a new file called pactSetup.js:




const path = require("path")
const Pact = require("@pact-foundation/pact")
global.port = 8081
global.provider = new Pact({
   port: global.port,
   log: path.resolve(process.cwd(), "__tests__/contract/logs", "logs-pact.log"),
   dir: path.resolve(process.cwd(), "__tests__/contract/pacts"),
   spec: 2,
   logLevel: 'INFO',
   pactfileWriteMode: "overwrite",
   consumer: "Frontend",
   provider: "ClientsService"
})

The first two lines of this file define the Pact provider on the consumer side. The consumer will use this information to create the provider mock and subsequent contracts as well. After port information is defined, various options for the Pact provider mock are set via a new instance of Pact.

NOTE

Logging paths will automatically be created on test execution.


NOTE

spec is simply a specification of Pact for other languages and is part of the contract structure. JVM uses a value of 3 while all other languages use a value of 2.


NOTE

A value of ā€œoverwriteā€ for pactfileWriteMode will overwrite and truncate existing Pact files on each test execution while a value of ā€œupdateā€ will append to a Pact file.


The next step is to create a Pact test wrapper as pactTestWrapper.js inside of the helpers folder.




jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000
beforeAll(() => provider.setup())
afterAll(() => provider.finalize())

Jasmine is used to set the timeout. The infrastructure code here simply sets up the provider prior to test execution and closes the provider mock afterwards.

Finally, some modifications to package.json are needed.


{
 "name": "tau-pact-nodejs-course",
 "version": "1.0.0",
 "description": "A simple Nodejs + jest project configuring contract tests with PactJS",
 "main": "src/index.js",
 "scripts": {
   "consumer": "node src/consumerService.js",
   "provider": "node src/providerService.js",
   "test:consumer": "jest __tests__/contract/consumer --runInBand --setupFiles ./__tests__/helpers/pactSetup.js --setupTestFrameworkScriptFile=./__tests__/helpers/pactTestWrapper.js",
   "test:provider": "jest __tests__/contract/provider --runInBand --force-exit"
 },
 "repository": {
   "type": "git",
   "url": "https://github.com/rafaelaazevedo/tau-pact-nodejs-course.git"
 },
 "keywords": [
   "nodejs",
   "contract-tests",
   "pactjs",
   "pact",
   "tutorial",
   "course",
   "tau"
 ],
 "author": "Rafaela Azevedo",
 "license": "MIT",
 "bugs": {
   "url": "https://github.com/rafaelaazevedo/tau-pact-nodejs-course/issues"
 },
 "homepage": "https://github.com/rafaelaazevedo/tau-pact-nodejs-course#readme",
 "devDependencies": {
   "@pact-foundation/pact": "^9.9.5",
   "axios": "^0.19.0",
   "body-parser": "^1.19.0",
   "cors": "^2.8.5",
   "eslint": "^6.8.0",
   "express": "^4.17.1",
   "jest": "^25.1.0",
   "superagent": "^5.2.2"
 },

 "jest": {
   "testEnvironment": "node"
 }
}

Inside of the scripts node create two new strings, test:consumer and test:provider. The first value maps to consumer tests and contract creation scripting while the second value maps to contract verification scripting. In this example both values are defined within the same project for the sake of convenience but in a real world scenario these scripts would appear only in their respective consumer and provider projects.

NOTE

--runInBand ensures that execution will take place in sequence.


NOTE

For test:provider, the --setupFiles parameter is not needed in this example since the provider will simply be executed. This flag is used with test:consumer to create the mock on the provider side.


This concludes our overview on setting up Pact. The next chapter will discuss test creation steps.



Resources



Ā© 2024 Applitools. All rights reserved. Terms and Conditions Privacy Policy GDPR