Flask application testing is usually associated with web frameworks which automate the tests performed across different web browsers, e.g. Selenium. However, not always there is a need to use such a complex tool and quite often most of the web application can be tested with the popular and universal pytest framework.
Exemplary web application
To start Flask and pytest tandem, let’s look at a minimal Flask application presented below.
Take into account that this is a very simple example and many consider it as the wrong way to build a Flask app. However, it is easy to follow, doesn’t require reading half of the Flask manual to understand what is going on with the application, works as expected and we can easily demonstrate how to match it with the pytest. You can run the code by placing it in any writable directory, going to this directory using the cd command and typing:
Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 184-072-487
which starts the Flask server at http://127.0.0.1:5000/ and allows you to make call to the application API. When we run the code, we obtain a file upload form with a comment textfield as below.
When you want to upload a file, you can use a to use a form with FileField.
class UploadForm(FlaskForm):
attachment = FileField(description="Attachment")
comment = StringField('Comment', description="Comment")
upload_btn = SubmitField('Upload', description="Upload")
The application accesses the file and the rest of the data posted in the form by the request object.
if request.files.get("attachment", None) and\
request.form.get("comment", None):
return render_template_string("Success")
The Content-Type header must be set to multipart/form-data, otherwise, the file upload will fail. You can read more about file uploads here
Testing a web-form
Now is time to test the application. You can stop the Flask server because the test client makes requests to the application without running a live server.
Your first step is to import the app – a Flask instance of the application being tested.
from app import flask_app
import pytest
@pytest.fixture(scope='module')
def app():
yield flask_app
The imported instance then exposes a test_client() method from Flask that contains the features needed to make HTTP requests to the application under test.
@pytest.fixture()
def client(app):
return app.test_client()
The client has methods that match the common HTTP request methods, such as client.get() and client.post(). To make a request, we call the method the request should use with the path to the route to test. The TestResponse class object is returned to examine the response data, which has all the usual properties of a response object. You’ll usually look at response.data, which contains the bytes returned by the view.
In our first test, we call the main page located at /. The code asserts that the responses received by test_client() are the HTML code of the main page. To verify our main page, it is often enough to search a text which the main page should contain, e.g. the form title.
def test_welcome_page(client):
response = client.get("/")
assert b"Upload data" in response.data
In the next step, we test the upload form with its file and text fields. To prepare the POST request, we create a dictionary with keys which match the form fields, i.e. attachment and comment. If a value for attachment is a file object opened for reading bytes (“rb” mode), it will be treated as an uploaded file. As file objects are to be closed after the request, they do not need to be preceded with the usual open() as f: pattern.
def test_uploaded(client):
file_name = "app.py"
data = {"attachment": (open(file_name, 'rb'), file_name),
"comment": "any comment"}
response = client.post('/uploaded', data=data, follow_redirects=True)
assert b"Success" in response.data
Test coverage
To have 100% coverage of the code, we need to test also the alternative scenario, e.g. when the file upload ends with a failure. To the code we add
def test_uploaded(client):
...
data = {"attachment": None, "comment": None}
response = client.post('/uploaded', data=data, follow_redirects=True)
assert b"Failure" in response.data
The whole pytest file is presented below. To run it, type
Using these simple test scenarios, you are able to obtain 100% coverage of your code! Type
$ coverage report
and you will get:
Name Stmts Miss Cover ------------------------------ app.py 19 0 100%
What about hyperlinks?
Forms are only one of the methods to interact with a web application. The other interactive elements are links. Both of them require different testing approaches. From the technical point of view, when using a form, we send data using the POST method, while when using a link, we use GET method. However, this issue does not significantly impact the testing. What is more important is the web page structure: namely, usually, we have only a single form in a web page which POST address is fixed and known. Url links, on the other hand, are multiple, often created dynamically and with variable GET destination address.
Lets look at an exemplary Flask application which generates a web page with 3 links
We want to test the serve_dessert() function, which generates a message with content depending on the selected link. We are interested only in the links directing to our application but not to the external web pages, so we mark them with a class_=”internal_url”.
The previous approach, where in the test function we provided the link to the POST method and sent a dictionary representing form fields is not applicable here because: we have multiple links with multiple URLs and we don’t know what are the links’ URLs. We need a different method.
Firstly, we have to find the links on the web page. So let’s look at the response object generated by a call to our website.
(Pdb) response
<WrapperTestResponse streamed [200 OK]>
(Pdb) response.data
<html>
Select a dessert
<ol>
<li><a class=internal_url href=/desserts?choice=Apple>Apple</a></li>
<li><a class=internal_url href=/desserts?choice=Orange>Orange</a></li>
</ol>
Other options
<ol>
<li><a href="https://deneb.click">External link</a></li>
</ol>
</html>
Beautiful Soup as a test tool
In the returned HTML code, we see the links and we can extract them using, e.g. search functions from the Python Standard Library. However, in the case of HTML, we have a better, more specialised tool – Beautiful Soup. Beautiful Soup is a Python package for parsing HTML (and other markup languages) documents. It creates a parse tree for parsed pages that can be used to extract data from HTML, which is handy for web scraping. So, to search for a pattern, we start by parsing the HTML code and creating the BeautifulSoup(response.data, ‘html.parser’) object. Then, using the object, we can quite easily extract needed data, namely links dessert_links, associated with them URLs link[‘href’] and their description link.get_text(). To exclude external links, which we don’t want to test, we limit the parser tree to internal links findAll(‘a’, class_=”internal_url”). After extracting the URLs, we follow them and get the response client.get(link[‘href’]). Finally, we check if the response includes the link’s name url_text.encode(‘utf-8’). I present the test function below:
def test_desserts(client):
response = client.get("/")
formated_response = BeautifulSoup(response.data, 'html.parser')
dessert_links = formated_response.findAll('a', class_="internal_url")
for link in dessert_links:
response = client.get(link['href'])
url_text = link.get_text()
assert url_text.encode('utf-8') in response.data
Finally, the whole source code for the pytest part.
Again, we achieved 100% test coverage (please, note that we already tested index() function in the previous examples).
Conclusions
In this post, I presented how to use the pytest to test simple, minimal web applications. You can explore much more functions of web applications testing with pytest in Flask’s official documentation. In my opinion, pytest offers great flexibility in creating tests and hence is a good choice as a test framework. It a universal tool which allows you to test any kind of application, whether it is a console, desktop or web. The most important thing is that you don’t have to learn a new testing tool every time you switch to a new environment as most of the concepts like fixtures and patches will work in the same way.