[PYTHON] Tutorial for doing Test Driven Development (TDD) in Flask-2 Decorators

Introduction

We will teach you the know-how necessary for test-driven development with Flask over a total of 5 times (which is planned and subject to change). In this second article, I'll show you how to use a decorator to compress the amount of code in your test code.

1st Tutorial for doing Test Driven Development (TDD) in Flask ―― 1 Test Client Edition Second time this article 3rd writing 4th writing 5th writing

Target audience

--For those who are going to develop web applications or APIs with Flask --Those who want to study test automation -1st time

Overview

Test code for APIs written in Flask with multiple endpoints, Compress the amount of code by using a decorator.

Directory structure

Place the sample code used in this article in the following directory structure.

flask_02/
├── Dockerfile
└── app
    ├── flask_app.py
    └── test
        ├── decorators.py
        ├── test1.py
        └── test2.py

docker version

$ docker --version
Docker version 19.03.12, build 48a66213fe

Code preparation

Dockerfile

Dockerfile


FROM python:3.6
USER root

RUN apt update
RUN /usr/local/bin/python -m pip install --upgrade pip
RUN pip install flask==1.1.2

COPY ./app /root/

WORKDIR /root/test

flask_app.py (tested)

Code for a Flask app with 3 endpoints.

flask_app.py


from flask import Flask
app = Flask(__name__)

@app.route('/hello_world')
def hello_world():
    return 'Hello, World!'

@app.route('/good_morning')
def good_morning():
    return 'Good, Morning!'

@app.route('/good_night')
def good_night():
    return 'Good, Night!'

if __name__ == '__main__':
    app.run(host="0.0.0.0",port=5000)

test1.py (test code)

For flask_app.py A test code that implements three test cases.

test1.py


import sys
sys.path.append('../')
import flask_app
import unittest

class Test_flask_app_Normal system(unittest.TestCase):

    def setUp(self):
        self.ENDPOINT    = "http://localhost:5000/{}"
        self.DATA        = None
        self.STATUS      = "200 OK"
        self.STATUS_CODE = 200
        self.ROUTE       = None
        
    def test_1_hello_Being able to access the world(self):
        # 1.Define test case specific variables
        self.DATA  = b"Hello, World!"
        self.ROUTE = "hello_world"
        
        # 2.Common parts of test cases
        with flask_app.app.test_client() as client:
            response = client.get(self.ENDPOINT.format(self.ROUTE))
        assert response.data        == self.DATA
        assert response.status      == self.STATUS 
        assert response.status_code == self.STATUS_CODE

        return

    def test_2_good_Access to morning(self):
        # 1.Define test case specific variables
        self.DATA  = b"Good, Morning!"
        self.ROUTE = "good_morning"
       
        # 2.Common parts of test cases
        with flask_app.app.test_client() as client:
            response = client.get(self.ENDPOINT.format(self.ROUTE))
        assert response.data        == self.DATA
        assert response.status      == self.STATUS 
        assert response.status_code == self.STATUS_CODE

        return

    def test_3_good_Access to night(self):
        # 1.Define test case specific variables
        self.DATA  = b"Good, Night!"
        self.ROUTE = "good_night"

        # 2.Common parts of test cases
        with flask_app.app.test_client() as client:
            response = client.get(self.ENDPOINT.format(self.ROUTE))
        
        assert response.data        == self.DATA
        assert response.status      == self.STATUS 
        assert response.status_code == self.STATUS_CODE

        return

if __name__ == '__main__':
    unittest.main()

test2.py (test code)

For flask_app.py A test code that implements three test cases. The content of the test being conducted is the same as test1.py, This code uses a decorator.

test2.py


import unittest
# 1.Load the decorator
from decorators import *

#Class name works in Japanese
class Test_flask_app_Normal system(unittest.TestCase):

    def setUp(self):
        self.ENDPOINT    = "http://localhost:5000/{}"
        self.DATA        = None
        self.STATUS      = "200 OK"
        self.STATUS_CODE = 200
        self.ROUTE       = None

    # 2.Modify decorator
    @get_test()
    def test_1_hello_Being able to access the world(self):    
        # 3.Define test case specific variables
        self.DATA  = b"Hello, World!"
        self.ROUTE = "hello_world"
        return

    # 2.Modify decorator
    @get_test()
    def test_2_good_Access to morning(self):
        # 3.Define test case specific variables
        self.DATA  = b"Good, Morning!"
        self.ROUTE = "good_morning"
        return

    # 2.Modify decorator
    @get_test()
    def test_3_good_Access to night(self):
        # 3.Define test case specific variables
        self.DATA  = b"Good, Night!"
        self.ROUTE = "good_night"
        return

if __name__ == '__main__':
    unittest.main()

decorators.py

Decorator used in test2.py.

decorators.py


import sys
sys.path.append('../')
import flask_app

#Decorator definition
def get_test():
    #Receiving a function for testing
    def recv_func(test_func):
        #Decorate the received test function
        def wrapper(self):
            # 1.Test case call
            test_func(self)
            # 2.Aggregation of common processes
            with flask_app.app.test_client() as client:
                response = client.get(self.ENDPOINT.format(self.ROUTE))
            assert response.data        == self.DATA
            assert response.status      == self.STATUS 
            assert response.status_code == self.STATUS_CODE    
        return wrapper
    return recv_func

Run the test

Check [Directory structure](#directory structure) and execute the following command.

$ ls
Dockerfile      app
$ docker build -t pytest .
~abridgement~
$ docker run -it pytest /usr/local/bin/python /root/test/test1.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.006s

OK
$ docker run -it pytest /usr/local/bin/python /root/test/test2.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.007s

OK

Explanation of test1.py (test code)

1. Define test case specific variables

In 1., the variables used in each test case are initialized. This time, the return value of the endpoint and API is stored. The endpoint and return value of the API are different because the endpoint and return value of the API to be tested are different. Initialization is performed with different values for each test case.

2. Common parts of test cases

In 2., using the test client, execute the API of flask_app.py and store the return value in response. Subsequent assert statements are used to compare the status, status code, and return value stored in the response. The same process is performed in the three test cases.

Disadvantages of writing test1.py

①. Increased cost of test-driven development

Specifically, because the parts that are common to each test case explained in 2. are not aggregated. Even minor corrections need to be made in all test cases. As a result, there is a possibility that it will take time to modify the test code in test-driven development.

②. Test code maintenance cost increases

For example, if for some reason the person who developed this test code is gone The successor has a large amount of test code, which can take a long time to decrypt. Also, in this test1.py example, there are only three test cases, Commercial test code is likely to have many test cases. Therefore, this writing method may increase the maintenance cost.

Explanation of decorators.py and test2.py (test code)

To solve the disadvantages of test1.py Aggregate the test code using a decorator.

Explanation of decorators.py

1. Call test case

Test cases decorated with the top-level function of nesting (get_test ()), It can be executed with test_func (self). Also, since the argument self is the same as the target test case self, The property defined by setUp (self) can be inherited.

2. Aggregation of common processes

The process written in the wrapper is It can be used in decorator-qualified test cases. That is, by writing the commonly used process in wrapper (self), It is possible to compress the amount of code. This time, the process of comparing the status, status code, and return value using the assert statement and the test client that is commonly used in the test case can be used in common.

Explanation of test2.py (test code)

1. Load the decorator

Load all decorator functions defined in decorators.py.

2. Modify the decorator

Qualify test cases with get_test () defined in decorators.py.

3. Define test case specific variables

The endpoint and API return values are different for each test case, so It cannot be aggregated into a decorator. Therefore, it is initialized in the test case.

Benefits of using decorators in your test code

It is possible to reduce the disadvantages described in [Disadvantages of writing test1.py](## Disadvantages of writing test1.py).

Summary

We introduced a method to reduce the amount of code in the test code by using a decorator in the test code. You can use it with other Flask apps just by rewriting the contents of wrapper (self) in test2.py.

next time

I will write an article about Fukahori of get method of test client and abnormal system test.

Recommended Posts

Tutorial for doing Test Driven Development (TDD) in Flask-2 Decorators
Tutorial for doing Test Driven Development (TDD) in Flask ―― 1 Test Client
[Test Driven Development (TDD)] Chapter 21 Summary
Test code for evaluating decorators
Learning history for participating in team app development in Python ~ Django Tutorial 5 ~
Learning history for participating in team app development in Python ~ Django Tutorial 4 ~
Learning history for participating in team app development in Python ~ Django Tutorial 1, 2, 3 ~
Learning history for participating in team app development in Python ~ Django Tutorial 6 ~
Learning history for participating in team app development in Python ~ Django Tutorial 7 ~
Implement Table Driven Test in Java
Test Driven Development with Django Part 4
Test Driven Development with Django Part 6
Test Driven Development with Django Part 2
Test Driven Development with Django Part 1
Test Driven Development with Django Part 5
Experience Part I "Multinational Currencies" in the book "Test Driven Development" in Python
Test Driven Development Startup with PySide & Pytest
(For myself) Put Flask in VS Code
Tips for building large applications in Flask
Django tutorial summary for beginners by beginners ⑤ (test)