Building Restful API with Flask, Postman & PyTest - Part 3 (Read Time: 20 Mins)

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.

happy giphy

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.

mic drop giphy

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

sad giphy

pytest1

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 testindexpage 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.

pytest2 happy giphy

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

foo fighters storie

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 testgetbalanceintransactions 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 testgetbalanceintransacations 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

book checking efteling

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

practice makes perfect 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.

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