Created on 25 Jan 2021 ;    Modified on 28 Jan 2021

How create a minimal flask project (part 6: adding unit tests)


This is the sixth part of an article about Flask, as follows:

Objective

This is the sixth part of a Flask project to show a single page in two different versions:

  • single language;
  • two languages.

Here we show how add unit tests to check our application.

Methodology

First, it's useful a little change in single_page/__init__.py. Then we are going to see how add unit tests.

Development

It's useful to modify the factory function create_app in single_page/__init__.py, otherwise during tests we have log mechanisms activated. This would be a little redondant. We would get an output in console as the follow:

(venv) >python -m unittest test_single_page.py
[2021-01-25 18:52:44,036] DEBUG in __init__: Covid application starts
.
----------------------------------------------------------------------
Ran 1 test in 0.064s

where the second line is a debugging log. Usually not very useful during a test session.

To avoid this we add a parameter to function create_app to set True during a test session. single_page/__init__.py become:

 1  ... <CUT> ...
 2 
 3  def create_app(test=False):                                 # +-
 4      '''create and configure the app'''
 5      app = Flask(__name__, instance_relative_config=True)
 6 
 7      # ensure the instance folder exists
 8      if not os.path.exists(app.instance_path):
 9          os.makedirs(app.instance_path)
10 
11      set_config(app)
12      if not test:                                             # +
13          set_logger(app)                                      # +-
14 
15      from .oneel import views as views1
16      app.register_blueprint(views1.oneel)
17 
18      from .twoels import views as views2
19      app.register_blueprint(views2.twoels)
20 
21      babel.init_app(app)
22      sitemap.init_app(app)
23 
24      @app.route('/sitemap')
25      def ep_sitemap():                            # endpoint for sitemap
26          return sitemap.sitemap(), 200, {'Content-Type': 'text/xml', }
27 
28      return app
29 
30  ... <CUT> ...

So we set logger only if parameter test is False.

Now the hard part. We go in flask_single_page/tests and we add the file tests/test_single_page.py:

 1  # import std libs
 2  import os
 3  import sys
 4  import unittest
 5 
 6  # import project's libs
 7  # we need to add the project directory to pythonpath to find project's module(s) in development PC without installing it
 8  basedir, _ = os.path.split(os.path.abspath(os.path.dirname(__file__)).replace('\\', '/'))  # basedir is the project's dir, one level up of our application
 9  sys.path.insert(1, basedir)                                                                # inserting in pythonpath @ ndx==1 because 0 is reserved for local directory
10  from single_page  import create_app
11 
12 
13  class InitTest(unittest.TestCase):
14      '''testing URLs of views'''
15 
16      def setUp(self):
17          self.app = create_app(test=True)
18 
19      def tearDown(self):
20          pass
21 
22      def test_sitemap(self):
23          with self.app.test_client() as client:
24              response = client.get('/sitemap')
25              xml = response.data.decode('utf8')          # type(html) == type(str)
26          self.assertEqual(response.content_type, 'text/xml')
27          self.assertTrue(xml.startswith('<?xml '))
28          self.assertIn('<loc>http://localhost/en/2l/index.html</loc>', xml)
29          self.assertTrue(xml.endswith('</urlset>'))
30 
31  if __name__ == '__main__':
32      unittest.main()

We need (line 4) the python's library unittest.

Then we need to import the application to test. In Python we can load a library only starting from directories that are present in the environment variable pythonpath (something like environment variable path where discover executables to run by the operating system). But our Python's environment knows anything about our (not installed) application. So, in lines 8 and 9 we insert our application base directory in pythonpath, so that we can import from module single_page its function create_app.

Class InitTest, mandatory derived from unittest.TestCase, is a folder to group all tests about a section of our module under exam.

Methods setUp (line 16) and tearDown are standard methods to execute before (setUp) and after (tearDown) each test. As we can see, we use setUp to initialize the application and store it in self.app.

Other methods (line 22) are our tests. Their names must start with prefix test. In our case, we have only one method, named test_sitemap.

In body of test_sitemap we call the Flask's self.app.test_client method. We can see this one as a sort of web server. Calling it with client.get('/sitemap') (@ line 24) give us a binary response equal to the response sent from our application to a web browser calling for url https://host:port/sitemap.

Next step (line 25) we decode from binary to utf-8 data code, and finally we test our result to check:

  • if it is a text/xml type (line 26),
  • if it starts with characters <?xml (line 27),
  • if in it there is the string <loc>http://localhost/en/2l/index.html</loc> (line 28),
  • if it ends with characters </urlset> (line 29).

As we tested for our module single_page, we can use unittest file(s) to test our single apps one by one.

Wait, how do we start tests? From folder tests we can fire tests about a single module calling as:

(venv) >python test_single_page.py
.
----------------------------------------------------------------------
Ran 1 test in 0.059s

OK

where we are firing tests about single_page.py module.

Otherwise we can call unittest library as a module. In this case unittest will discover each python file in current directory whose name starts with prefix test and will run it:

(venv) >python -m unittest
.
----------------------------------------------------------------------
Ran 1 test in 0.057s

OK

Please, don't stop here. There is a lot to discover digging in python's unittest environment. And about the importance of test activities.

Enjoy, ldfa