Introduction
Today in our final part of the 3 part series I will be covering the creation of actual REST APIs with PyTest.
For those new the series, you can look at part 1 to understand the various tools that I will be using to create REST API endpoints of a expanses manager.
Besides that look at part 2 in mocking the API endpoints for prototyping your API designs.
Tool
Source Code
Endpoints To Create
Now for the last part of the series, we will be covering the CRUD functions that use only the GET HTTP request method.
Since the creation of PUT, POST and DELETE endpoints is identical, it would be better to introduce the use of Pytest.
The Endpoints and API Documentation the Final Part:
The API Documentation in Postman
- Get List of Transactions - GET
- Create a New Transaction - POST
- Update an Individual Transaction - PUT
- Delete an Individual Transaction - DELETE
Project Setup
Creating a Project Folder
Create your project in Linux, by creating a folder called expanses_manager.
Next in the create the virtual environment using pipenv and switch to the python virtual environment.
pipenv install
pipenv shell
Installing Python Packages
Now you need to install the following by typing the command below.
pipenv shell
pipenv install Flask flask-cors pytest pytest-cov pytest-flask requests pylint
With the basic python packages installed, we shall proceed with creating our first Flask app.
Creating Your First Flask App
Create a file called expanses_manager.py, then open your IDE or Editor of choice to make changes to the content of the file.
expanses_manager.py
from flask import Flask # Import the flask web server
app = Flask(__name__) # Single module that grabs all modules executing from this file
@app.route('/') # Tells the flask server on which url path does it trigger which for this example is the index page calling "hello_world" function.
def hello_world():
return 'Hello, World!'
Executing Your Flask App
Now once you have added the code to the file, we need to run the following command start this flask app:
export FLASK_APP=expanses_manager.py
flask run
Congrats you had just created your first flask app. You can open up your browser and type the following url "127.0.0.1:5000" to your browser. To cancel you need to press ctrl + c to quit the server.
Development Mode In Flask
Type the following command so that we shall enable development mode.
In this mode, your flask app has a debugger and automatically restarts by themselves whenever there is a change in the code.
export FLASK_ENV=development
flask run
Understanding Routing
Routing is the way you assign for a specific web page to load. It can be a simple example like below:
@app.route('/') # Routes you to the index page
def index():
return 'Index Page'
@app.route('/hello') # Routes you to the page with http://127.0.0.1:5000/hello/
def hello():
return 'Hello, World'
@app.route('/projects/') # URL with trailing slash
def projects():
return 'The project page'
@app.route('/about') # URL without a trailing slash
def about():
return 'The about page'
It can be very simple to very complex I won't be covering much since we are focusing on building the GET function of the expanses manager.
I will attach a reference link for you to the link section for a better understanding of routing.
Do note that by default if you do not include a url with a trailing slash which is this /.
Flask will automatically redirect you a 404 when you add a page with / at the end of the url.
Creating Your First REST API Endpoint
If you had not read the earlier parts of the series, please go to part 2 or 3 series to understand what is HTTP methods and status code.
You can look at the example below by changing your expanses_manager.py code to the following to understand how does it work.
from flask import Flask, request, jsonify # Imports the flask library modules
app = Flask(__name__) # Single module that grabs all modules executing from this file
@app.route('/login', methods=['GET', 'POST']) # HTTP request methods namely "GET" or "POST"
def login():
data = []
if request.method == 'POST': # Checks if it's a POST request
data = [dict(id='1', name='max', email='max@gmail.com')] # Data structure of JSON format
response = jsonify(data) # Converts your data strcuture into JSON format
response.status_code = 202 # Provides a response status code of 202 which is "Accepted"
return response # Returns the HTTP response
else:
data = [dict(id='none', name='none', enmail='none')] # Data structure of JSON format
response = jsonify(data) # Converts your data strcuture into JSON format
response.status_code = 406 # Provides a response status code of 406 which is "Not Acceptable"
return response # Returns the HTTP response
Now you can open up your Postman to create a request called Testing Login Request.
Enter this URL http://127.0.0.1:5000/login to your request and set HTTP method to be either GET or POST request then click send to get the response result.
Creating Bash Script for Configuration Settings
We will be creating a bash script that allows the executing of the above settings without constantly typing the command.
Create a file that is called env.sh and fill the contents of the file with the code below.
env.sh
#!/usr/bin/env bash
pipenv shell
export FLASK_ENV=development FLASK_APP=expanses_manager.py
flask run
Once you had created the file called env.sh, you need to change the file permission using this command:
$ chmod 775 env.sh
To run the bash script, you need to be at the project root folder using the command below.
$ ./env.sh
Creating your First Test Case
Now we shall start by creating our first test case for your flask API endpoint.
We shall test if the flask app is running by checking if the index.page is serving an HTTP response.
Creating test_endpoints.py
So let's create test script call test_endpoints.py and add in the following code to your newly created file:
test_endpoints.py
import pytest
import requests
url = 'http://127.0.0.1:5000' # The root url of the flask app
def test_index_page():
r = requests.get(url+'/') # Assumses that it has a path of "/"
assert r.status_code == 200 # Assumes that it will return a 200 response
Executing the test_endpoints.py
Have the flask app is running in the first terminal. Then create a second terminal and run pytest using the command below:
$ pytest
Did you see a F beside your test_endpoints.py which means that your test case has failed.
Please do not be disturbed as it is expected behaviour on our first try since we did not include an index endpoint that returns a response.
To fix your test script, you can choose to follow the pytest suggestion by changing the status code to 400 instead of 200 and execute the test again.
The test does pass with a "." yet this will not be able to check if your flask app is running under the root folder and returns an HTTP 200 response.
Therefore we need to create the index endpoint instead of fixing our test cases.
Create your index endpoint
Since the test case in test_index_page requires an endpoint with a path of / and an HTTP response of 200.
We shall add the index page in our expanses_manager.py:
expanses_manager.py
@app.route('/', methods=['GET'])
def index_page():
response = jsonify('Hello World!!!')
response.status_code = 200
return response
Now type pytest -v in the 2nd terminal while your 1st terminal is executing the flask app.
The command pytest -v provides you with more information about your test cases which is useful for debugging your test cases and your python endpoints as well.
Now after executing the previous command, you should see that your test cases are passing. Awesome job!!!! In creating your first endpoint and test case.
Let's take a well-deserved rest and move on to the next section when you are ready to dive further.
Creating Expenses Manager Endpoints
Since you had started to create your first endpoint in Flask & test cases using Pytest.
Let's review on how the list of APIs you need to create to create a expanses manager:
List of API to Create:
- Get List of Transactions - GET
- Create a New Transaction - POST
- Update an Individual Transaction - PUT
- Delete an Individual Transaction - DELETE
Get List of Transactions
Now start with the first GET endpoint which provides a list of transactions.
What is the bare minimum test that we need to create to check if it works?
Creating User Stories
User stories are useful to figure out what needs to be created before implementing.
We will be taking this user story script to build the user test case:
As (role of the user), I want to (the activity) so that (desired result)
So it will look like something like this when creating the user story
As a User, I want to have a snapshot of my expenses so that I know where am I spending my money
Creating Test Case for Balance In the Expenses Manager
With this user story, we can build a bare minimum test case. Which is to show the balance that we have in our account.
test_endpoints.py
def test_get_balance_in_transacations():
r = requests.get(url+'/transactions/')
assert r.status_code == 200
Now let's run your newly created test_get_balance_in_transactions test case then.
Since you did not create a transactions endpoint, it fails asking you to change your test case to be 404.
For it to pass we need to call an endpoint called transactions in expanses_manager.py.
expanses_manager.py
@app.route('/transactions/', methods=['GET'])
def list_of_transactions():
response = jsonify({})
response.status_code = 200
return response
With that you had pass your first test. Now let's create another test case that checks the content of the response to have a balance of 0.
test_endpoints.py
def test_get_balance_in_transacations():
r = requests.get(url+'/transactions/')
assert r.status_code == 200
data = r.json()
assert data[balance'] == 0
Expect your test cases to fail if you run the above code. We need to modify expanses_manager.py for it to pass.
expanses_manager.py
@app.route('/transactions/', methods=['GET'])
def list_of_transactions():
response = jsonify({'balance': 0})
response.status_code = 200
return response
When u run it again you will see that it passes for this test case.
Have you Completed the User Story?
So now's my question to you did we fulfil our user story for this test case?
If not what are the things that are still missing? A good guess will be the mock endpoints you had created in part 2.
Since we know that besides balance of the expenses manager, we need to get the number of transactions founded in the endpoint.
Before that, we need to clean up our code for this test_get_balance_in_transacations endpoint.
test_endpoints.py
def test_get_balance_in_transacations():
r = requests.get(url+'/transactions/')
data = r.json()
assert r.status_code == 200
assert data['balance'] == 0
Get Number of Transactions in Transactions Endpoint
Once you had done with this code, create a new test case that tests the number of transactions in the Transactions endpoint.
test_endpoints.py
def test_get_number_of_transacations():
r = requests.get(url+'/transactions/')
data = r.json()
assert r.status_code == 200
assert len(data['transactions']) != 0
This test case tests the total number of transactions must be more than 0.
Once again your test case fails so let's create a transaction for the test case to pass.
expanses_manager.py
@app.route('/transactions/', methods=['GET'])
def list_of_transactions():
response = jsonify({'balance': 0,
'transactions': [
{}
]})
response.status_code = 200
return response
Checking the Fields of an Individual Transaction
As you had done so far, we had only checked if it has more than 0 transactions.
We had failed in checking if the transactions have the same fields as the mock endpoints found in part 2 of the series.
So now let's create these tests to check for the fields within the transactions.
test_endpoints.py
def test_individual_transaction_fields():
r = requests.get(url+'/transactions/')
data = r.json()
fields = list(data['transactions'])
assert r.status_code == 200
assert fields[0]['amount'] >= 0.00
assert fields[0]['current_balance'] < 240
assert 'jean' in fields[0]['description']
assert 0 < fields[0]['id']
assert 300 == fields[0]['inital_balance']
assert "2019-01-12 09:00:00" == fields[0]['time']
assert fields[0]['type'] != 'income'
Again when you run this initially it fails, so now is the time to add in the fields of the individual transactions.
expanses_manager.py
@app.route('/transactions/', methods=['GET'])
def list_of_transactions():
response = jsonify({'balance': 0,
'transactions': [
{'amount': 0.0, 'current_balance': 230, 'description': 'blue jean', 'id':2, 'inital_balance': 300, 'time': "2019-01-12 09:00:00", 'type': 'expense'}
]})
response.status_code = 200
return response
Refactoring To a Single Class
Now we shall refactor your 4 test cases and consolidate it into a single class for ease of running tests which we call it test suite.
Which you can enter this command to test that specific test suite
pytest -v test_endpoints.py::NameOfTheSuite
test_endpoints.py
class TestTransactions():
def test_index_page(self):
r = requests.get(url+'/')
assert r.status_code == 200
def test_get_balance_in_transacations(self):
r = requests.get(url+'/transactions/')
data = r.json()
assert r.status_code == 200
assert data['balance'] == 0
def test_get_number_of_transacations(self):
r = requests.get(url+'/transactions/')
data = r.json()
assert r.status_code == 200
assert len(data['transactions']) != 0
def test_individual_transaction_fields(self):
r = requests.get(url+'/transactions/')
data = r.json()
fields = list(data['transactions'])
assert r.status_code == 200
assert fields[0]['amount'] >= 0.00
assert fields[0]['current_balance'] < 240
assert 'jean' in fields[0]['description']
assert 0 < fields[0]['id']
assert 300 == fields[0]['inital_balance']
assert "2019-01-12 09:00:00" == fields[0]['time']
assert fields[0]['type'] != 'income'
Basics of Test Driven Development
If you had not noticed that the constant failure and refactoring to pass your test cases is a software development practice called Test Driven Development.
Which can be used to complement it with Pytest to create various test cases for your future projects.
Basic Process of TDD
- Create a failed test case
- Implement code to pass
- Refactor code and start from the top again
Remaining Endpoint Stories
Here are the remaining endpoint user stories for you to create which I will provide the source code in two weeks in my GitHub repo for this companion tutorial series:
Create a New Transaction - As a User, I want to have a record my expenses so that I know on where my money is going
Update an Individual Transaction - As a User, I want to edit a specific transaction so that ** I had the correct balance**
Delete an Individual Transaction - As a User, I want to remove a transaction so that ** I had the right balance in my expenses manager**
Conclusion
If you had been with me so far into the end of this series, I would like to thank you for taking the time to go through this 3 part series as it is no mean feat to write it.
Remember that I will be releasing the solution for this series of the remaining endpoints for POST, PUT & DELETE HTTP requests in about two weeks time.
It has been an honour in writing this 3 part series, I hope what you had learnt can be useful in helping you in creating RESTful API endpoints in Flask using PyTest, Postman and TDD techniques.
Links
Expanses Manager Source Code
API Documentation In Postman
Writing Test Cases Form User Stories From Acceptance Criteria
User Story
Test Driven Development: what it is, and what it is not.
Python Testing With Pytest
Flask
PyTest
HTTP Request Method
HTTP Status Codes
If you like my article either sign up for Max's Weekly Newsletter or you can follow to get the latest update of my article on Dev
This post was originally posted on max's blog at Building Restful API with Flask, Postman & PyTest - Part 3 (Read Time: 20 Mins) and picture from Photo by Rachael Gorjestani on Unsplash
Top comments (0)