Build, Test, and Deploy a Flask Application: Part 4
Restructuring testing
Meta Information About This Tutorial
Learning goals
- Unit testing
Note: This tutorial is part of the series Learn Flask in a scientific way.
Source code
- The source code of this tutorial can be accessed on GitHub.
- The demo can be accessed at https://pacific-fortress-91193.herokuapp.com/.
- The version that covers only this tutorial can be accessed here.
Install Your Application as a Package
As we move to use Application Factory and Blueprints, our testing code also needs some upgrade. To make our application that adopts Application Factory and Blueprints visible to testing code underneath a different folder, we need to install our application as a package first. There are three steps to achieve this.
First, we need to record all the package dependencies. This is important, especially when you need to replicate what you have in a different machine/virtual environment (e.g., deployment). To do so, navigate to the root of the directory (mean-review-collector
) through your terminal and type:
(env) $ pip freeze >requirements.txt
You will see a file requirements.txt
generated at the root directory that contains both package names and their exact version numbers.
Flask==1.1.1
Jinja2==2.10.3
...
wcwidth==0.1.7
Werkzeug==0.16.0
zipp==0.6.0
When you want to build a replica in a different virtual environment, you can just type:
(env) $ pip install -r requirements.txt MANIFEST.in
Second, we need to describe our project. The description relies on two files: setup.py
and MANIFEST.IN
. These two files are needed to make our app
installable. setup.py
contains the basic information about our application:
from setuptools import find_packages, setupsetup(
name='app',
version='0.1',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=[
'flask',
],
)
include_package_data=True
indicates that we want to include extra data for this project. MANIFEST.IN
includes basic information about the data of this project, such as templates and sql
file:
include app/schema.sql
graft app/static
graft app/templates
global-exclude *.pyc
Third, we need to install our own code in app
as a package. Using pip install
, the installation is only one line:
(env)$ pip install -e .
However, install
deserves some detailed explanation. Normally, when you install Python packages through pip nstall
, pip
looks for the target package in the Python Package Index. However, pip can also look for packages which are in other places (e.g., code in your local machine).
The above one-line code essentially copies your code over to the env
folder and treats it like any other package you've installed in this virtual environment. The dot refers to your current directory. The flag -e
makes it an editable install — if you make further changes to the files inside the project folder, those will be automatically reflected in the env
folder as well.
Now you can check all the installed packages in this virtual environment:
(env)$ pip list
You will see something like:
Package Version Location
------------------ ------- ---------------------------------------
app 0.1 /home/qiang/repos/mean-review-collector
attrs 19.3.0
Click 7.0
Flask 1.1.1
...
Unit Testing
The next thing we do is to rewrite our unit testing cases. We used unittest
simply for convenience. From now on, we will switch from unittest
to pytest
. Although pytest
is not in the standard library of Python, it is better than unittest
in many ways. To install pytest
and another tool coverage
(that measures code coverage of your testing), we just need to type through your terminal:
(env)$ pip install pytest coverage
By default, pytest
only identifies the test files or methods whose names start with test_
or end with _test
. We have at least three areas to test, including the factory, the database, and the authentication, so we need three files underneath tests
directory:
mean-review-collector
- env
- app
- instance
- tests
- test_auth.py
- test_db.py
- test_factory.py
When all the test code is ready, you can run all of them by simply typing pytest to your terminal, and you may see something similar to:
(env)$ pytest
=============== test session starts ===========================
platform linux -- Python 3.7.4, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /home/qiang/repos/mean-review-collector
collected 7 itemstests/test_auth.py .. [ 42%]
tests/test_db.py .. [ 71%]
tests/test_factory.py .. [100%]
Before we rush to write the testing code in each file, we should recognize that certain code always needs to be executed first before any unit testing, such as connecting to the database and initiating an application object. pytest
uses fixture
to take care of such code. The purpose of fixtures is to provide a fixed baseline upon which tests can reliably and repeatedly execute. To achieve this, we would put such code in a different file named conftest.py
. For instance, we certainly need some code to help us initiate an application object:
#conftest.py@pytest.fixture
def app():
db_fd, db_path = tempfile.mkstemp() app = create_app({
'TESTING': True,
'DATABASE': db_path,
}) with app.app_context():
init_db() yield app os.close(db_fd)
os.unlink(db_path)
pytest
uses the decorator to identify a function as a fixture — a function that should be executed first before each testing.
Although the testing on database and authentication was rather similar to our old testing code that used unittest
, there are still two differences that we want to highlight.
First, parametrize can be used to test various inputs against a function:
# test_auth.py@pytest.mark.parametrize(('username', 'password', 'message'), (
('', '', b'Username is required.'),
('a@c.com', '', b'Password is required.'),
('a@b.com', 'test', b'already registered'),
))
def test_register_validate_input(client, username, password, message):
response = client.post(
'/register',
data={'username': username, 'password': password}
)
assert message in response.data
This is achieved using pytest.mark
decorators for testing functions.
Second, monkeypatch can be a useful tool for testing. The following code essentially follows the guidance of Flask’s official tutorial:
def test_init_db_command(runner, monkeypatch):
class Recorder(object):
called = False def fake_init_db():
Recorder.called = True monkeypatch.setattr('app.db.init_db', fake_init_db)
result = runner.invoke(args=['init-db'])
assert 'Initialized' in result.output
assert Recorder.called
However, the official tutorial doesn’t really explain monkeypatch. monkeypatch dynamically changes a piece of software (e.g., a module, object, method, or function) at runtime. Pytest uses this feature to allow the testing of functions or methods that you don’t want to actually execute.
For instance, we have a function named get_info as the following:
def get_info():
"""
call GET for http://XXX/get
returns status code and url in HTTP response
"""
r = requests.get(base_url + 'get')
if r.status_code != 200:
return r.status_code, ''
else:
response_data = r.json()
return r.status_code, response_data["url"]
We can test this function out without making it actually send an HTTP request by using monkeypatch:
def test_get_info(monkeypatch):
class ResponseMock(object):
def __init__(self):
self.status_code = 200
self.url = ''
self.headers = {}
def json(self):
return {'account': '123','url': 'http://test.com'}
def get_patched(url):
return ResponseMock()
monkeypatch.setattr(requests, 'get', get_patched)
assert get_info() == (200, 'http://test.com')
What monkeypatch realizes here is essentially to replace the behavior of requests.get()
with what you have defined under get_patched
. Similarly, in our test_init_db_command
method:
monkeypatch.setattr('app.db.init_db', fake_init_db)
We essentially attached some new behavior to app.db.init_db
. The new behavior is being defined under fake_init_db
. The only thing this method actually achieves is adding a new field Recorder.called for app.db.init_db
to keep track of whether it gets executed or not.
Please check out the final code of our restructured unit testing here.
Tutorial List
- Build and test a mini Flask application
- Build, Test, and Deploy a Flask Application: Part 1 — Templates
- Build, Test, and Deploy a Flask Application: Part 2 — Authentication
- Build, Test, and Deploy a Flask Application: Part 3 — Application Factory and Blueprints
- Build, Test, and Deploy a Flask Application: Part 4 — Restructuring Testing
- Build, Test, and Deploy a Flask Application: Part 5 — Authentication (continued)
- Build, Test, and Deploy a Flask Application: Part 6 — Review System
- Build, Test, and Deploy a Flask Application: Part 7 — Deployment