Building Restful API with Flask, Postman & PyTest - Part 3 (Read Time: 20 Mins)
Tags: python, webdev, showdev, beginners
Published: 2019-02-03 13:19:53

---
title: Building Restful API with Flask, Postman & PyTest - Part 3 (Read Time: 20 Mins)
published: true
description: Building Expenses Manager API with Flask, PyTest and TDD
tags: python, webdev, showdev, beginners
canonical_url: https://www.maxongzb.com/building-restful-api-with-flask-postman-and-pytest-part-3-read-time-20-mins/
---

![Photo by Rachael Gorjestani on Unsplash](https://thepracticaldev.s3.amazonaws.com/i/x0j4u0q0fm8twxlshtt4.jpg)
## 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][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][2] in mocking the API endpoints for prototyping your API designs. 

## Tool
* [PyTest][3]
* [Postman][4]
* [Git][5]
* [Visual Studio Code][6]

## Source Code
* [Exapanses Manager Repo][7]

## 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 ][8] 
* 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][9]

### 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][10]

## 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][11] 

![pytest1][12]

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. 

![pytest2][13] ![happy giphy][14]

## 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][15]

**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
![book checking efteling][16]

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][17]
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][7]
### [API Documentation In Postman][8]
### [Writing Test Cases Form User Stories From Acceptance Criteria][18]
### [User Story][19]
### [Test Driven Development: what it is, and what it is not.](https://medium.freecodecamp.org/test-driven-development-what-it-is-and-what-it-is-not-41fa6bca02a2)
### [Python Testing With Pytest][20]
### [Flask][21]
### [PyTest][3]
### [HTTP Request Method][22]
### [HTTP Status Codes][23]


If you like my article either sign up for [Max's Weekly Newsletter](http://eepurl.com/dOUoUb) 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)](https://www.maxongzb.com/building-restful-api-with-flask-postman-and-pytest-part-3-read-time-20-mins/) and picture from [Photo by Rachael Gorjestani on Unsplash](https://unsplash.com/photos/X6CZGpJBi8U/info)


[1]: https://dev.to/steelwolf180/building-restful-api-with-flask-postman--pytest---part-1read-time-6-mins-15b4
[2]: https://dev.to/steelwolf180/building-restful-api-with-flask-postman--pytest---part-2-read-time-10-mins-1d6d
[3]: https://docs.pytest.org/en/latest/
[4]: https://www.getpostman.com/
[5]: https://git-scm.com/
[6]: https://code.visualstudio.com/
[7]: https://github.com/steelwolf180/expanses_manager
[8]: https://documenter.getpostman.com/view/4542874/RztitA2G
[9]: //images.ctfassets.net/ly2f59p4unnn/76Ku1K7YENHrA550LOPLAU/0ea27c050c07c43c7bb1dbe7331ad510/happy_giphy.gif
[10]: //images.ctfassets.net/ly2f59p4unnn/4J8cA5UWpCyxyNiofVcdO1/830f9326a4a7903cb21f067ccfd5f91a/mic_drop_giphy.gif
[11]: //images.ctfassets.net/ly2f59p4unnn/2HwQC1oQFu1LkzUtGZT4Ez/3f3b4d42900bf4a53486a9d8f40af17c/sad_giphy.gif
[12]: //images.ctfassets.net/ly2f59p4unnn/31VfT8yB4gryosbrlv0aSX/07f297cf539e71f8de50b426ad3218e9/pytest1.png
[13]: //images.ctfassets.net/ly2f59p4unnn/3wjoHfbNiV7N4WUFmhhdm7/ac0e91696436e819cb9f5da62259977d/pytest2.png
[14]: //images.ctfassets.net/ly2f59p4unnn/3NQcfgJZ1CxNspBPkS3eBz/4f5a92c82b156560d247ee8b7375ded0/happy_giphy.gif
[15]: //images.ctfassets.net/ly2f59p4unnn/2IQ1LTkNyWQihoFSlIS4NR/1a9ae6b1daf415383ecdd90b4e146c66/foo_fighters_storie.gif
[16]: //images.ctfassets.net/ly2f59p4unnn/5IWyUCc83CrmrSvlcVaOqw/8ff28ccab673a056463302b528b2ccef/book_checking_efteling.gif
[17]: //images.ctfassets.net/ly2f59p4unnn/01lKoShJbMJQYoEs44XtoQ/f8f8e819316599990d13108b01b412b2/practice_makes_perfect.gif
[18]: https://blog.testlodge.com/writing-test-cases-from-user-stories-acceptance-criteria/
[19]: https://www.scaledagileframework.com/story/
[20]: https://www.amazon.com/Python-Testing-pytest-Effective-Scalable/dp/1680502409
[21]: http://flask.pocoo.org/
[22]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
[23]: https://www.restapitutorial.com/httpstatuscodes.html