
Writing integration tests for API's is challenging in a micro-services world as it involves multiple API's from multiple components.
In this article we will be focusing on the two major challenges of writing API Integration Testing. It also talks about how to overcome them by using modern testing tools and techniques. We will be usingPactumJS to write automated API integration test cases.
PactumJS
REST API Testing Tool for all levels in a Test Pyramid
PactumJS is a REST API Testing Tool used to automate e2e, integration, contract & component (or service level) tests.
Documentation
This readme offers an basic introduction to the library. Head over to the full documentation athttps://pactumjs.github.io
Need Help
We use GithubDiscussions to receive feedback, discuss ideas & answer questions.
Installation
# install pactum as a dev dependencynpm install --save-dev pactum# install a test runner to run pactum tests# mocha / jest / cucumbernpm install --save-dev mocha
or you can simply use
npx pactum-init
Usage
…Challenges
These are the two things of many that I personally felt challenging while writing API Integration Tests.
- Passing data across tests.
- Retry on failed expectations.
Example
It's always better to have an example to understand the core concepts of a topic under discussion.
Let's take a simple example of an e-commerce application with the following API endpoints for processing an order.
- POST
/api/orders
(for placing an order) - POST
/api/payments
(for making a payment) - GET
/api/payments/{payment_id}
(for fetching the status of payment)
Workflow
To make things clear, the requests and responses shown below are overly simplified.
Step 1 - Place Order
A user comes in and makes aPOST request to/api/orders
with the following payload to place an order.
Request Payload
{"product":"PlayStation 5"}
At the time of writing this article, it's highly impossible to buy the above product. At-least in some places. 🙂
Now the server responds with the following response body which contains the orderid
.
Response
{"id":"1f4c99e9-12df-45d4-b455-98418f4e3b1e"}
This orderid
is dynamically generated by the API server. We need to grab it and pass it to the other endpoints likepayments to complete the order.
Using any testing library, we can save the response in a variable and use them later. It works but not efficient. Because when we write integration tests for large scale applications, it forces us to pass significant amount of data between tests and API calls. Declaring intermediary variables will damage the readability of the code.
To overcome this challenge PactumJS comes with a concept ofData Store to pass data between API calls across tests.
Let's look at the test first.
awaitpactum.spec().post('/api/orders').withJson({"product":"PlayStation 5"}).expectStatus(200).stores('OrderID','id');
The above test will make aPOST request to/api/orders
with givenjson
payload and once the response is received it expects the status should be200
andstores the value ofid
into a special variable calledOrderID
which is internal to PactumJS.
Step 2 - Make Payment
The next step is to make the payment. Now the user makes aPOST request to/api/payments
with the following payload.
Request Payload
{"order_id":"1f4c99e9-12df-45d4-b455-98418f4e3b1e","card_info":{"number":"1111-1111-1111-1111","expiry":"11/11","cvv":"111"}}
Now the API responds with the following response body which contains paymentid
.
Response
{"id":"a32fce50-d4e8-4d95-b16f-57fd13fbb7df"}
Now let's talk about the test case.
As you observed, theorder id
from the previous request is included in the request payload.
To get the value of special internal variable, PactumJS uses a special pattern -$S{<variable-name>}
to access it.
Let's look at the test.
awaitpactum.spec().post('/api/payments').withJson({"order_id":"$S{OrderID}","card_info":{"number":"1111-1111-1111-1111","expiry":"11/11","cvv":"111"}}).expectStatus(200).stores('PaymentID','id');
PactumJS will internally replace$S{OrderID}
with1f4c99e9-12df-45d4-b455-98418f4e3b1e
before making the request.
In the above test case we are also saving the paymentid
into the special variablePaymentId
using thestores method. Using the paymentid
we can track the status of the payment. So this brings us to the final step of our integration test.
Step 3 - Wait for Payment to be completed.
To get the status of the payment, user makes aGET request to the/api/payments/{payment_id}
endpoint.
The API responds with the following response body.
Response
{"status":"in-progress"}
As you see, the status is stillin-progress
. We need to wait for few seconds for the payment to be completed.
Including hard waits is a bad practice in testing. PactumJS comes with a concept ofretry mechanism which retries on failed expectations. It is similar tofluent wait in selenium.
Let's look at the test case.
awaitpactum.spec().get('/api/payments/{id}').withPathParams('id','$S{PaymentID}').expectStatus(200).expectJson({"status":"completed"}).retry();
By default it retries3
times with a delay of1000ms
between each retry.
Complete Test
Now let's take a look at the entire test usingPactumJS andmocha.
constpactum=require('pactum');
it('order PS5 and make payment',async()=>{
awaitpactum.spec()
.post('/api/orders')
.withJson({
"product":"PlayStation 5"
})
.expectStatus(200)
.stores('OrderID','id');
awaitpactum.spec()
.post('/api/payments')
.withJson({
"order_id":"$S{OrderID}",
"card_info":{
"number":"1111-1111-1111-1111",
"expiry":"11/11",
"cvv":"111"
}
})
.expectStatus(200)
.stores('PaymentID','id');
awaitpactum.spec()
.get('/api/payments/{id}')
.withPathParams('id','$S{PaymentID}')
.expectStatus(200)
.expectJson({
"status":"completed"
})
.retry();
});
Conclusion
Writing readable and maintainable tests is very important to make API testing productive and enjoyable experience.
PactumJS abstracts the challenging parts to write tests in an easy and fun way and ultimately making API Integration testing super easy.
Top comments(3)
For further actions, you may consider blocking this person and/orreporting abuse