DEV Community 👩‍💻👨‍💻

Cover image for A guide to testing flask applications using unittest!
Phantz
Phantz

Posted on

A guide to testing flask applications using unittest!

Flask is a backend web framework for python. One that is beloved by many python developers! But to make a great flask webapp, testing is important. I noticed the lack of a good library to integrate with the stdlib unittest for testing flask apps. So I made one myself!

Check out flask-unittest on github!

This tutorial will demonstrate how to test flask applications using the FlaskClient object, i.e in an API centric way, and also demonstrate how to test flask applications live, using a headless browser!

If you're looking to use a FlaskClient to test your app, I recommend you to read through the official testing guide. It's super simple and should only take 5 minutes to read through!

Test using a FlaskClient object

The library provides us with the testcase ClientTestCase, which creates a FlaskClient object from a Flask object for each test method, that you can use to test your app. It also provides direct access to flask globals like request, g, and session!

Let's see how you could use ClientTestCase

import flask_unittest

from flask_app import create_app

class TestFoo(flast_unittest.ClientTestCase):
    # Assign the flask app object
    app = create_app()

    def test_foo_with_client(self, client):
        # Use the client here
        # Example request to a route returning "hello world" (on a hypothetical app)
        rv = client.get('/hello')
        self.assertInResponse(rv, 'hello world!')
Enter fullscreen mode Exit fullscreen mode

We have a flask app in a module named flask_app, which has a function to create and return the Flask app object. Just need to assign the returned object to app.

Remember, you don't need a function to create and return the app. As long as you have a correctly configured app object, you can simply assign it!

Now, we define a test method test_foo_with_client with 2 parameters. The mandatory self and a parameter named client. For each test method, ClientTestCase will create a FlaskClient by using .test_client and pass it to the test method.

Now you can freely use this to make API calls to your app! In our example, we make a request to /hello, which is a simple route returning hello world! as a response. You can now use assertInResponse, which is a utility method provided by flask-unittest, to check if hello world! actually exists in the response! (Note: you can also just use assert 'hello world!' in rv.data for the same effect)

Inside this test method, you also have access to flask globals like request, g, and session.

def test_foo_with_client(self, client):
    rv = client.get('/hello?q=paramfoo')
    self.assertEqual(request.args['q'], 'paramFoo')    # Assertion succeeds
    # Do a POST request with valid credentials to login
    client.post('/login', data={'username': 'a', 'password': 'b'})
    # Our flask app sets userId in session on a successful login
    self.assertIn('userId', session)    # Assertion succeeds
Enter fullscreen mode Exit fullscreen mode

This is obviously very useful for testing!

You can also use the setUp method to login to your webapp, and the session will persist in the actual test method! Because setUp, tearDown and the test method are ran together in a set - using the same FlaskClient. The next test method along with its setUp and tearDown methods, however, will use a brand new FlaskClient - and hence a new session.

def setUp(self, client):
    # Login here
    client.post('/login', data={'username': 'a', 'password': 'b'})

def test_foo_with_client(self, client):
    # Check if the session is logged in
    self.assertIn('userId', session)    # Assertion succeeds

def tearDown(self, client):
    # Logout here, though there isn't really a need - since session is cleared for the next test method
    client.get('/logout')
Enter fullscreen mode Exit fullscreen mode

There are also multiple utility methods for common assertions!

  • assertStatus - Assert the status code of a response returned from a client API call.
  • assertResponseEqual - Assert the response .data is equal to the given string
  • assertJsonEqual - Assert the response .json is equal to the given dict
  • assertInResponse - Assert given string/bytestring exists in response .data
  • assertLocationHeader - Assert the location header in response is equal to the given string. Useful for redirect requests.

Check out a full example of using this testcase in the repo

Would you like the Flask app to be built per test method too? Instead of having it as a constant property of the class? Check out AppClientTestCase (or even AppTestCase if you don't need the functionality of the FlaskClient)

Test a live flask server using selenium

What if you want to just run a regular flask server live and use a headless browser like selenium to test it out? LiveTestCase and LiveTestSuite is for you!

import flask_unittest
from selenium.webdriver import Chrome, ChromeOptions
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

from flask_app import create_app

class TestFoo(flask_unittest.LiveTestCase):
    driver: Union[Chrome, None] = None
    std_wait: Union[WebDriverWait, None] = None

    @classmethod
    def setUpClass(cls):
        # Initiate the selenium webdriver
        options = ChromeOptions()
        options.add_argument('--headless')
        cls.driver = Chrome(options=options)
        cls.std_wait = WebDriverWait(cls.driver, 5)

    @classmethod
    def tearDownClass(cls):
        # Quit the webdriver
        cls.driver.quit()

    def test_foo_with_driver(self):
        # Use self.driver here
        # You also have access to self.server_url and self.app
        # Example of using selenium to go to index page and try to find some elements (on a hypothetical app)
        self.driver.get(self.server_url)
        self.std_wait.until(EC.presence_of_element_located((By.LINK_TEXT, 'Register')))
        self.std_wait.until(EC.presence_of_element_located((By.LINK_TEXT, 'Log In')))
Enter fullscreen mode Exit fullscreen mode

This is pretty simple, we instantiate the driver in setUpClass, use it as we would normally in the test methods and quit it in tearDownClass.

You'll have access to the flask app as well as the url the app is running on (localhost + port) in the testcase. (only in the instance methods though)

The real juice of this is actually in LiveTestSuite. Unlike the previous testcases, which can be run using the regular unittest testsuite, or simply doing unittest.main() - LiveTestCase requires you to use LiveTestSuite

# Pass the flask app to suite
suite = flask_unittest.LiveTestSuite(create_app())
# Add the testcase
suite.addTest(unittest.makeSuite(TestFoo))
# Run the suite
unittest.TextTestRunner(verbosity=2).run(suite)
Enter fullscreen mode Exit fullscreen mode

We have to pass the flask app object to LiveTestSuite, so it can spawn it using .run - this same app object will be available in the testcase.

The flask server will be spawned as a daemon thread once the suite starts running and it runs for the duration of the program.

Check out a full example of using this testcase in the repo!

For more examples, including a full emulation of the official flask testing example, check out the repo!

Top comments (3)

Collapse
rogerperkins profile image
rogerperkins

In the example above, there is a misspelling in this line with the class, instead of flast_unittest with a t, it should be flask_unittest:

class TestFoo(flast_unittest.ClientTestCase):

should be this:

class TestFoo(flask_unittest.ClientTestCase):

That was really messing me up, thanks.

Roger

Collapse
rogerperkins profile image
rogerperkins • Edited on

I installed via: pip install flask-unittest, but when I try to import flask_unittest, it doesn't work, I think it's because the package name has a dash (-) in it instead of an underscore (_). Tried to import flask-unittest, the import statement doesn't seem to like dashes. Referring to the Pycharm messages by the way.

Collapse
totally_chase profile image
Phantz Author

The package name can be different than the module name. Generally, pip packages use dashes but python modules don't allow dashes in them. That said, you should be able to use the package with import flask_unittest. Make sure you're in the same environment where the package is installed.

🌚 Browsing with dark mode makes you a better developer by a factor of exactly 40.

It's a scientific fact.