Testing Flask applications is often associated with complex cross-browser automation tools like Selenium. However, many web applications can be effectively tested using the popular and versatile pytest framework, eliminating the need for overly complicated solutions.
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-487This starts the Flask server at http://127.0.0.1:5000/ and exposes the application API. When you run the code, you'll see a file upload form with a comment field:
To handle file uploads, we 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 uploaded file and form data through 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 for file uploads to work correctly. You can read more about file uploads here.
First, import the 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 exposes a test_client() method from Flask that provides HTTP request capabilities for testing:
@pytest.fixture()
def client(app):
return app.test_client()
The client supports common HTTP methods like client.get() and client.post(). To make a request, call the appropriate method with the route path. A TestResponse object is returned, which has properties like response.data (containing the bytes returned by the view).
In our first test, we check the main page at / by verifying it contains expected text (e.g., the form title):
def test_welcome_page(client):
response = client.get("/")
assert b"Upload data" in response.data
Next, we test the upload form with its file and text fields. To prepare the POST request, create a dictionary with keys matching the form fields: attachment and comment. If the value for attachment is a file object opened in binary read mode ("rb"), it will be treated as an uploaded file. File objects don't need to be explicitly closed using open() as f: syntax, as they're automatically closed after the request.
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
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 complete pytest file is shown below. To run the tests:
These simple test scenarios will give you 100% coverage of your code! Run:
Name Stmts Miss Cover ------------------------------ app.py 19 0 100%
Forms are just one way to interact with a web application—links are another common interactive element. While forms typically use the POST method and links use GET, this technical difference doesn't significantly impact testing. The more important distinction is in page structure: forms are often single with fixed POST addresses, while links are numerous, often dynamic, with variable GET destination addresses.
Let's look at a Flask application that generates a web page with 3 links:
We want to test the serve_dessert() function, which generates messages based on the selected link. We're only interested in internal links (those pointing to our application), so we mark them with class_="internal_url".
The previous approach (providing a fixed POST endpoint and form data) doesn't work here because we have multiple links with unknown URLs. We need a different method to find and test these links.
First, examine the response object from the 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="http://deneb.click">External link</a></li>
</ol>
</html>
To use Beautiful Soup:
Here's the test function:
def test_desserts(client):
response = client.get("/")
formatted_response = BeautifulSoup(response.data, 'html.parser')
dessert_links = formatted_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, here's the complete pytest code:
Again, we've achieved 100% test coverage (note that the index() function was already tested in previous examples).
In my opinion, pytest is an excellent choice for a test framework due to its great flexibility. It's a universal tool that can test any type of application—console, desktop, or web. Most importantly, you don't need to learn a new testing tool when switching environments, as core concepts like fixtures and patches work consistently across project types.