Python API test automation framework (Part 2) Making HTTP requests

Image for post
Image for post

Logos in header image sources: Python, Requests, JSON, HTTP

This is second post in a series on how to build an API framework using python.

Read part 1 here

Understanding API under test

Let’s understand the API that we will be using in this tutorial a bit better.

We will be using people-api which is a set of CRUD HTTP operations developed using Python Flask, SQLAlchemy and uses sqlite as the database and represents a list of persons with first name, last name and an id

To setup clone people-api repo from github and then activate the pipenv by running below:

pipenv shell

Ensure all the dependencies are installed in the pipenv by executing:

pipenv install

Once done, Init the database by executing

python build_database.py

This would seed the local sqlite database with some dummy records

Finally then start the HTTP service on your local by executing and ensuring this keeps on running during the test

python server.py

We can see from the log that the local service has started, you would also see logs for any request made to the server

* Serving Flask app "config" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 105-008-664

Let’s navigate to http://0.0.0.0:5000 to see the application,

Note: To see the swagger spec for this API we can navigate to http://0.0.0.0:5000/api/ui/#/

As you can see this API supports basic CRUD operations (CREATE, READ, UPDATE, DELETE)

Image for post
Image for post

If you want to play around with these requests, I have also included a postman collection inside this repo under postman folder with all these basic operations already setup.

The API supports creating a person, updating him/her, deleting the person and able to read a user or all the users in the database. We will see these operations while writing tests.

First test, read via GET API

Let’s see how we can create a basic test to make this HTTP request via python using requests module that we have already imported

The curl for this API is:

curl --location --request GET 'http://0.0.0.0:5000//api/people' \ --header 'Accept: application/json'

Below is the code snippet that wires up a basic HTTP GET request and then asserts that the service returns us a 200 status code and that the returned list of people has kent in it.

import requests
from assertpy.assertpy import assert_that
from config import BASE_URI

def test_read_all_has_kent():
# We use requests.get() with url to make a get request
response = requests.get(BASE_URI)
# response from requests has many useful properties
# we can assert on the response status code
assert_that(response.status_code).is_equal_to(requests.codes.ok)
# We can get python dict as response by using .json() method
response_text = response.json()
first_names = [people['fname'] for people in response_text]
assert_that(first_names).contains('Kent')

Notice we use assert_that from assertpy module for fluent assertions, The failure messages from these are quite readable and we would discuss more about this in a future post. Please ensure you have that installed in your virtualenv using pipenv install assertpy

BASE_URI = 'http://0.0.0.0:5000/api/people'

In above, we are using BASE_URI variable to get the base api url to be used. This is stored in a config.py file as below:

Creating a person using POST method

Alright, now that we have the basic case of GET API taken care of, let’s see how we can make a POST request.

Generally in Rest API’s when you need to create a resource you would use POST method

Below is the cURL to make the request, our python code is going to provide the same headers, body and URL to hit the request

curl --location --request POST 'http://0.0.0.0:5000//api/people' \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --data-raw '{ "fname": "TAU", "lname": "User" }'

Here is the complete flow that we want to automate:

  • Create a new person in the db
  • Verify if the newly created user is present using the GET API
  • Filter to find if the created data is present in all the users in the system

Here Ln 2: calls create_new_person() where we first create the body to be passed as part of the request

Notice on Ln 14 we are using dumps() method from json module to convert python dict to json string (serialization) and using str(uuid4()) to get a unique last name on Ln 13 to ensure we don't have conflicting data on every test run

Finally we make the POST request using

And then use search_created_user_in() method to see if the newly created user is present in the database

Deleting a person using DELETE method

How would we go about the delete method?

In this API we can delete a person by passing in the person_id, it is always a good practice to create your own test data before trying delete operations instead of using any existing data since that could result in non-deterministic tests

So here it the flow:

  • Create a new person
  • Get the person id using read API
  • Hit the delete endpoint and assert

Since we already have methods to create and then read users, the only thing left is to implement the DELETE operation by ensuring the person_id is passed in the API path params, We use the fantastic f-strings to create our URL and then hit delete using requests.delete() method.

def test_created_person_can_be_deleted():
persons_last_name = create_new_person()

peoples = requests.get(BASE_URI).json()
newly_created_user = search_created_user_in(peoples, persons_last_name)[0]

delete_url = f'{BASE_URI}/{newly_created_user["person_id"]}'
response = requests.delete(delete_url)
assert_that(response.status_code).is_equal_to(requests.codes.ok)

Bonus

Note in assertions we are not specifying numeric value for the status codes, requests provides a lookup dictionary wherein you can easily specify the code using readable english like syntax and this is definitely better than parsing what code X means on MDN — Mozilla developer network every time 😉

In other words instead of 200 we could write it as requests.codes.ok

assert_that(response.status_code).is_equal_to(requests.codes.ok)

Conclusion

This is just a taste of what requests as a library can do. For more details on what all features it provides please read requests docs and I hope this gives you a good idea on how to use requests to make HTTP calls. In the next post we would discuss more on working with JSON/XML data types and assertions. Stay tuned. ⏰

And here are reads from MDN on Content-Type and Accept headers

If you found this post useful, Do share it with a friend or colleague and if you have thoughts, I’d be more than happy to chat over at twitter or comments. Until next time. Happy Testing and Coding.

Originally published at https://automationhacks.io on November 27, 2020.

Lead SDET at Gojek, Bengaluru, I ❤️ to code in Kotlin, Python 🐍, and Java to build scalable test automation frameworks. Blog at automationhacks.io 🇮🇳

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store