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.

An Example Web Application

To get started with pytest and Flask, let's examine a minimal Flask application: This is intentionally a very simple example—many developers would consider this an unconventional way to build a Flask app. However, it's easy to follow, requires minimal Flask knowledge, works correctly, and clearly demonstrates how to use pytest for testing. To run the code: 1. Save it to any writable directory 2. Navigate to that directory using the `cd` command 3. Run:
$ FLASK_DEBUG=1 flask run
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
This 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:

Testing web applications with pytest

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.

Testing Web Forms

Now it's time to test the application. You can stop the Flask server—the test client makes requests directly to the application without requiring a live server.

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

Test Coverage

To achieve 100% code coverage, we need to test the failure scenario for file uploads. Add this test case:

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:

$ pytest test_app.py

These simple test scenarios will give you 100% coverage of your code! Run:

$ coverage run -m pytest
$ coverage report
You'll see:
Name        Stmts   Miss Cover
------------------------------
app.py      19      0    100%

Testing Hyperlinks

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>

Beautiful Soup as a Test Tool

While you could extract links using Python's standard library search functions, HTML parsing is better handled with a specialized tool like Beautiful Soup. This Python package parses HTML (and other markup languages) and creates a parse tree for easy data extraction—ideal for web scraping.

To use Beautiful Soup:

  1. Parse the HTML response to create a BeautifulSoup(response.data, 'html.parser') object
  2. Extract the desired data: links (dessert_links), URLs (link['href']), and link text (link.get_text())
  3. Limit extraction to internal links using findAll('a', class_="internal_url") to exclude external links
  4. Follow each internal link and verify the response contains the link text

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).

Conclusion

This post demonstrates how to use pytest to test simple, minimal Flask web applications. For more advanced testing capabilities, refer to Flask's official documentation.

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.