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!')
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
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')
There are also multiple utility methods for common assertions!
-
assertStatus
- Assert the status code of a response returned from aclient
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')))
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)
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)
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
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.
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.