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
--For those who are going to develop web applications or APIs with Flask --Those who want to study test automation -1st time
Test code for APIs written in Flask with multiple endpoints, Compress the amount of code by using a decorator.
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 19.03.12, build 48a66213fe
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
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)
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()
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()
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
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
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.
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.
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.
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.
To solve the disadvantages of test1.py Aggregate the test code using a decorator.
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.
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.
Load all decorator functions defined in decorators.py.
Qualify test cases with get_test ()
defined in decorators.py.
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.
It is possible to reduce the disadvantages described in [Disadvantages of writing test1.py](## Disadvantages of writing test1.py).
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.
I will write an article about Fukahori of get method of test client and abnormal system test.
Recommended Posts