How to Write Unit Tests in Python, Part 3: Web Applications

Posted by
on under

This is the third part in my series about unit testing Python applications. In this article I'm going to show you some of the techniques I use when I work with Python web applications. For the examples in this article I will be using the Flask framework (I know, what a surprise!), but the concepts I'll show you are universal and can be applied to other frameworks as well.

While this is the first article in the series that covers web applications, I will be applying many of the ideas and solutions I shared in the first and second parts, so be sure to check those out first, even if you are only interested in web applications.

Preparing the Application for Testing

All the examples in this article are based on my Microblog application, from the Flask Mega-Tutorial. If you'd like to follow along using this application, you can clone it from GitHub with the following command:

$ git clone https://github.com/miguelgrinberg/microblog

To set up the application to run on your system, create a virtual environment and install the requirements. Then initialize the database as follows:

(venv) $ flask db upgrade

Now the application is ready to run. Use the following command to start the server:

(venv) $ flask run

Visit http://localhost:5000 on your web browser to view this application in action.

This is also a good time to set up our testing infrastructure, which requires installing the pytest and pytest-cov packages:

(venv) $ pip install pytest pytest-cov

Given that the Microblog application already has a limited number of unit tests in file tests.py in the top-level directory, you can now run these tests with the following command:

(venv) $ pytest --cov=app --cov-report=term-missing --cov-branch test*

According to the test output, there are four unit tests in the tests.py file, and they amount to a coverage of 40%, which is better than no coverage at all, but also far from ideal.

Testing Traditional Web Applications

In this first section I'm going to show you how to write unit tests for a web application built in the "traditional" model, where the majority of the application logic is located in the server. This type of application generates HTML in the server through templates and has limited or no use of JavaScript in the browser.

The four tests that I featured in the Flask Mega-Tutorial are testing very specific features of the application in isolation. This is a good strategy to use for parts of the application that can be executed without having the entire application running. Consider the four areas that I have covered:

  • Password hashing
  • Generation of user avatar URLs
  • Followers
  • Timeline of followed posts

The only requirement to exercise these parts of the application is access to a database, and in fact, these four tests target functionality that can be accessed from the User model alone. There is no need to invoke web routes, render templates, or work with a web requests, so this type of tests are the simplest to create, and can be targeted to the basic building blocks of your application, those that cannot fail under any circumstance.

Creating a Test Application Instance

But let's rewind and start from the beginning. As you recall from the previous articles, my preferred way to structure unit tests is in classes, using the TestCase base class from the unittest package. In the unit tests that you've seen on those articles the use of test classes wasn't really providing any benefit, since each test was independent of the others and there was no object-level state being maintained.

Using a TestCase class is really helpful, in my opinion, when you have tests that require common set up procedures. For example, all the tests that are featured in this article need a Flask application instance, which in the Microblog application is created with the create_app() application factory function. Having to call create_app() at the start of every unit test would be very tedious, so we are going to take advantage of a facility provided by the TestCase class to define common initialization and destruction tasks in a single place.

Below you can see is a minimal skeleton test case class for Microblog. Since we are going to be testing the web application routes, you can write this code in a file called test_microblog.py in the root directory of the application:

import unittest
from flask import current_app
from app import create_app


class TestWebApp(unittest.TestCase):
    def setUp(self):
        self.app = create_app()
        self.appctx = self.app.app_context()
        self.appctx.push()

    def tearDown(self):
        self.appctx.pop()
        self.app = None
        self.appctx = None

    def test_app(self):
        assert self.app is not None
        assert current_app == self.app

The setUp() and tearDown() methods of the TestCase class are automatically invoked before and after each test, so they are the ideal place to insert common logic that applies to all the tests in the class.

In the setUp() method I am creating the application instance, which I store in the self.app attribute. Then I create an application context, which in Flask is often needed. The context is saved in self.appctx and for convenience I'm also pushing it into the Flask application context stack.

You can see that the test_app() test method makes sure that self.app is defined, and also that current_app is set to the application, which should happen when the application context is pushed.

The tearDown() method runs after each test. Here you are supposed to undo anything that was done in setUp(). So I pop the application context, and reset the two attributes back to None to revert the class to a clean state.

Run pytest as above one more time to also include this new test case. When I run it here, my coverage increased to 42%, since we are now exercising the create_app() application factory function.

Using a Test Database

If the web application you intend to write unit tests for uses a database, you have to decide if you are also going to use a database during your tests, or if instead the database calls will be mocked.

For an application that does minimal database work, it may make sense to mock database queries in unit tests. In the previous part in this series I showed how to mock a function call using mock.patch. The process to mock the database works in a similar way, with the database query calls being replaced with mocks. To do this effectively you will need to manually run some real database queries and take note of their return values, so that you can then inject realistic query results in your mocked database calls.

I personally find that for projects that are non-trivial mocking the database requires a lot of work, so in general I prefer to use an actual test database. For this to work your application needs to support the database details given as configuration, so that the tests can be configured to use a separate database.

If your application uses a SQLite database, then you have the option to use an in-memory database for the tests, which is great because in-memory databases are very fast. If using SQLAlchemy, this is done by using sqlite:// as database URL, without providing a database filename.

Below you can see how I modified the test_microblog.py file to also initialize an in-memory SQLite database:

import os
os.environ['DATABASE_URL'] = 'sqlite://'  # use an in-memory database for tests

import unittest
from flask import current_app
from app import create_app, db


class TestWebApp(unittest.TestCase):
    def setUp(self):
        self.app = create_app()
        self.appctx = self.app.app_context()
        self.appctx.push()
        db.create_all()

    def tearDown(self):
        db.drop_all()
        self.appctx.pop()
        self.app = None
        self.appctx = None

    def test_app(self):
        assert self.app is not None
        assert current_app == self.app

To make sure that all tests use an in-memory SQLite database, I set the DATABASE_URL environment variable directly in the global scope, above all other imports so that by the time the config.py file is imported, this variable is already set correctly. Then in the setUp() method I call db.create_all() to initialize the in-memory database with empty tables for all the models defined in the application. Note that for db.create_all() to work an application context is required, so this needs to be done after the context is pushed. In tearDown() I'm calling db.drop_all() to destroy all databases. This is technically unnecessary when you are using an in-memory database, but I prefer to do it anyway, in case in the future I decide to run the test suite on a different database.

Using a Test Client

In Python, the communication between a web server and the actual web application is well standardized, and this is great, because it allows generic web servers such as Gunicorn or uWSGI to serve web applications written with any Python web framework.

The most widely used communication protocol is called WSGI, or Web Server Gateway Interface. The two most popular web frameworks for Python, Flask and Django, use this interface. A large number of smaller frameworks also use WSGI.

Since the introduction of the asyncio package in Python 3.4, several new web frameworks designed for the asynchronous development model have appeared. Since the WSGI interface cannot be easily adapted to work asynchronously, a new protocol called ASGI, or Asynchronous Server Gateway Interface, has been created. A popular web framework that uses ASGI is FastAPI, but as with WSGI, there are many more frameworks that have also adopted ASGI.

What do WSGI and ASGI have to do with unit testing? Both standards have very specific rules regarding how the web server needs to pass an incoming request to the web application. This is very convenient, because a unit test can follow the same procedure to inject fake requests into the application during testing, without the need to have a real client or a real web server running.

All three web frameworks mentioned above provide "test clients" that inject made-up requests into the application for the purposes of testing. In your test code, this ends up looking more or less like sending a real request with the requests package. However, the test client does not do any networking, the fake requests are all sent directly to the application without a server involved.

Even if you are using a framework that does not provide its own test client, as long as the framework adheres to WSGI or ASGI you can use a generic test client with it. For WSGI applications, you can use the test client included in the Werkzeug package. For ASGI, you can use async-asgi-testclient.

Here is the test_microblog.py file showing how I extended the setUp() method to also create a test client:

import os
os.environ['DATABASE_URL'] = 'sqlite://'  # use an in-memory database for tests

import unittest
from flask import current_app
from app import create_app, db


class TestWebApp(unittest.TestCase):
    def setUp(self):
        self.app = create_app()
        self.appctx = self.app.app_context()
        self.appctx.push()
        db.create_all()
        self.client = self.app.test_client()

    def tearDown(self):
        db.drop_all()
        self.appctx.pop()
        self.app = None
        self.appctx = None
        self.client = None

    def test_app(self):
        assert self.app is not None
        assert current_app == self.app

    def test_home_page_redirect(self):
        response = self.client.get('/', follow_redirects=True)
        assert response.status_code == 200
        assert response.request.path == '/auth/login'

The test_home_page_redirect() test sends a GET request to the top-level URL of the application. The follow_redirects flag is set so that the test client automatically handles redirect responses. For this application, the home page requires the user to be logged in, so the response is going to be a redirect to the login page. The response.request attribute can be used to retrieve details of the originating request. in this case I'm using it to make sure that the path that was requested after the redirect is the one for the login page.

Testing for Specific HTML Content

One of the most difficult aspects of testing a web application is making sure the HTML returned is correct. In my view it is important to strike a good balance here, because if you go out of your way to test every little aspect of your HTML, then whenever you make changes to the page your tests will start to fail and will need to be updated. You really want to "spot check" the HTML to make sure it has the right components, without requiring a complete match. For example, if you are testing that a form was rendered, you can search the HTML for the fields of the form and the submit button, but not look for them to be in a specific order, or in specific part of the page.

Below you can see a test that I created to make sure the user registration page is correct:

    def test_registration_form(self):
        response = self.client.get('/auth/register')
        assert response.status_code == 200
        html = response.get_data(as_text=True)

        # make sure all the fields are included
        assert 'name="username"' in html
        assert 'name="email"' in html
        assert 'name="password"' in html
        assert 'name="password2"' in html
        assert 'name="submit"' in html

Here I'm sending a GET request to the registration page, making sure it returns a 200 status code. Then I extract the HTML from the response object using response.get_data(). The default for the response is to be returned as a bytes object, so I request that it is converted to text as a matter of convenience.

To make sure that the form elements are all in the page I decided to check that for each field the string name="field-name" is in the page. Typically a form field will be rendered with the format <input ... name="field-name" ...>, so I consider it sufficient to just check for the name. If you prefer to do a more involved check, you can always use the re package and search the HTML with regular expressions.

Submitting Forms

The next problem is how to write unit tests for user actions that require submitting a form. As I'm sure you know, a form is normally submitted by the browser in a POST request, with the form fields passed in the body of the request.

The test client makes it easy to submit a POST request with form fields, but what can actually be a complication is to handle the CSRF protection that most frameworks implement in forms. This is a feature designed to prevent an external agent from submitting a form on your behalf and without you realizing they are doing it. It is implemented with a hidden field that is added to all forms, set to a randomly generated token, the so called CSRF token. The form submission needs to include this token in addition to all the normal fields, because when this token is missing the server rejects the submission as invalid.

One possible approach to submit forms is to first issue a GET request for the form page, so that you can fish out the CSRF token from the HTML. Once you have the CSRF token, you can add it to the form submission, and then the application will have no trouble accepting the form. While this is doable, I prefer to take a more practical approach, which is to disable CSRF protection while the tests run.

In Flask, CSRF protection can be disabled by setting the WTF_CSRF_ENABLED to False in the configuration. This can be incorporated into the setUp() method:

    def setUp(self):
        self.app = create_app()
        self.app.config['WTF_CSRF_ENABLED'] = False  # no CSRF during tests
        self.appctx = self.app.app_context()
        self.appctx.push()
        db.create_all()
        self.client = self.app.test_client()

Below you can see a new unit test that registers a user, and then logs in with that user to confirm that the registration worked:

    def test_register_user(self):
        response = self.client.post('/auth/register', data={
            'username': 'alice',
            'email': 'alice@example.com',
            'password': 'foo',
            'password2': 'foo',
        }, follow_redirects=True)
        assert response.status_code == 200
        assert response.request.path == '/auth/login' # redirected to login

        # login with new user
        response = self.client.post('/auth/login', data={
            'username': 'alice',
            'password': 'foo',
        }, follow_redirects=True)
        assert response.status_code == 200
        html = response.get_data(as_text=True)
        assert 'Hi, alice!' in html

The test starts by sending a POST request to the registration URL, which is exactly what the browser does when you press the submit button in the form. The Flask test client accepts the form fields in the data argument, using a dictionary. The keys of the dictionary must match the form field names.

When the Microblog application receives a user registration, it redirects the user back to the home page, which in turn redirects to the login page. The user must immediately log in with the selected username and password to access the application. So following the form submission, I make sure that I'm now in the login page, and then issue a second form submission with the username and password, this time to log in to the application.

As a result of logging in, I will be sent to the home page of the application, which has a heading with the format `Hi, [username]!'. The test makes sure the HTML of the return page contains this greeting.

Testing Form Validation

Another important group of tests are those that make sure that forms are not accepted unless they have valid data. As an example of that, below you can see a new user registration test that makes sure that the form isn't accepted when the two password fields do not match:

    def test_register_user_mismatched_passwords(self):
        response = self.client.post('/auth/register', data={
            'username': 'alice',
            'email': 'alice@example.com',
            'password': 'foo',
            'password2': 'bar',
        })
        assert response.status_code == 200
        html = response.get_data(as_text=True)
        assert 'Field must be equal to password.' in html

For this test I make sure that the error message added by the form validation appears in the HTML of the response.

Testing Pages that Require Authentication

Most applications have sections that are restricted to logged in users. You may be tempted to take a similar approach to CSRF and just add a configuration option that removes the login requirement during testing, but authentication has a very important function besides restricting access, which is to allow the server to know who the client is. In Flask, this would be the current_user variable, which is one of the most commonly used application building blocks. If authentication is disabled, then current_user will not be set, and that is a problem. So in terms of authentication, the tests will perform a log in, exactly how real users do it.

You saw in the previous section that it is easy enough to log in to the application with a POST request to the /auth/login URL, but to avoid having to repeat this procedure in every test that needs a logged in user I like to implement some shortcuts.

First of all, I want to always have a user I can login with in my database. The best place to do this this is in the setUp() method:

    def setUp(self):
        self.app = create_app()
        self.app.config['WTF_CSRF_ENABLED'] = False  # no CSRF during tests
        self.appctx = self.app.app_context()
        self.appctx.push()
        db.create_all()
        self.populate_db()
        self.client = self.app.test_client()

The only change here is that I've added a call to the populate_db() method, which I'm going to use for adding initial data to my test database. Here is the definition of this method:

# ...
from app.models import User

class TestWebApp(unittest.TestCase):
    # ...

    def populate_db(self):
        user = User(username='susan', email='susan@example.com')
        user.set_password('foo')
        db.session.add(user)
        db.session.commit()

An interesting feature of the TestCase class is that any methods that do not start with test_ are ignored by the testing framework, so you are free to add any auxiliary methods that you need.

With these changes the database that is made available to each test will be initialized with the user susan. So now I can add another auxiliary method that performs the login:

    def login(self):
        self.client.post('/auth/login', data={
            'username': 'susan',
            'password': 'foo',
        })

in this case I do not worry with adding assertions, because we have already written a test that ensures that the login is working. This method is just a helper that we are going to call from any tests that require a user to be logged in, so that we don't repeat this over and over again.

Let's write a test that posts a message. This is a core feature of the Microblog application, and is a great way to ensure the logged in user is handled correctly, because the author of the post is going to be extracted from the current_user variable. Here is the code for this new test:

# ...
import re

class TestWebApp(unittest.TestCase):
    # ...

    def test_write_post(self):
        self.login()
        response = self.client.post('/', data={'post': 'Hello, world!'},
                                    follow_redirects=True)
        assert response.status_code == 200
        html = response.get_data(as_text=True)
        assert 'Your post is now live!' in html
        assert 'Hello, world!' in html
        assert re.search(r'<span class="user_popup">\s*'
                         r'<a href="/user/susan">\s*'
                         r'susan\s*</a>\s*</span>\s*said', html) is not None

For this test I can begin by calling self.login(), which will do the login form submission with the susan user created in the setUp() method. Then I can freely issue a form submission for the form that posts a new message, and this message will be attributed to the user I just logged in with.

When a submission is accepted the message "Your post is now live!" appears in a notification bar at the top of the page, so I can make sure that this text is in the HTML returned in the response. Next I can also ensure that the text of the message posted is in the page.

To check that the message was attributed to the correct user I decided to incorporate a regular expression search. I first executed the application manually, and created a message with the same username and same text as in the unit test above. Then I opened the HTML page source in the browser to learn how the message is rendered. Here is the section of HTML that renders the message:

<span class="user_popup">
  <a href="/user/susan">
    susan
  </a>
</span>
 said 
<span class="flask-moment" data-timestamp="2021-07-25T23:04:32Z" data-function="fromNow" data-refresh="0" style="display: none">
  2021-07-25T23:04:32Z
</span>
<br>
<span id="post1">Hello, world!</span>

This is fairly complex, but I don't really need to verify every single thing here. I just want to make sure that the "susan said" portion is there. Unfortunately there is some HTML markup between the words "susan" and "said", so I decided to use the regular expression to check for the markup as well, which is good because that also ensures that the user is rendered with the proper formatting.

The regular expression checks for the <span> and <a> elements, and adds a \s* in between elements to allow any amount of whitespace. This is so that the test does not break if the spacing or alignment of the elements change in the future.

Note that I am not checking what follows after the word "said". This is going to be a timestamp to be rendered by the Flask-Moment extension that has no relevance in this test.

Testing API Servers

All the examples I showed you so far are meant to test a traditional web application. The other web application design style is the one called Single-Page Application or SPA. In applications that follow this style, the server takes on a smaller role centered around the storage, while the client implements most of the application logic. In this model, the server is often called an "API server".

Testing APIs is actually easier, first because these servers have a much smaller scope, and second because in general modern API servers use JSON as data format, and JSON is much easier to parse than HTML.

Registering a User Through the API

Earlier I showed you how to test the user registration flow, which involved testing that the registration form rendered correctly, and then doing a form submission. Let's now test the user registration flow, as done from the Microblog API.

When using the API, a user is registered with a POST request to /api/users. The username, email and password fields are given in JSON format in the body of the request. Below you can see the test that registers a user:

    def test_api_register_user(self):
        response = self.client.post('/api/users', json={
            'username': 'bob',
            'email': 'bob@example.com',
            'password': 'bar'
        })
        assert response.status_code == 201

        # make sure the user is in the database
        user = User.query.filter_by(username='bob').first()
        assert user is not None
        assert user.email == 'bob@example.com'

As you can see, this is much simpler than the version using HTML and forms, here we just need to send an HTTP request and check that the status code in the response is 201. As an additional measure, I decided to use the User model to check that the user actually exists in the database.

Token Authentication

The user registration endpoint is the only API entry point that is open to all users. Every other endpoint requires the user to authenticate. APis in general use an authentication mechanism that is different than traditional web applications. Instead of a session-based authentication, APIs prefer a stateless mechanism based on tokens.

The Microblog application requires the client to send a POST request to /api/tokens to request a token. The request must include a basic authentication header with the username and password of the user requesting the token. The returned token can then be passed as a bearer token header when making requests to other endpoints.

Following the same general idea of the login() helper method, let's now add a get_api_token():

    def get_api_token(self):
        response = self.client.post('/api/tokens', auth=('susan', 'foo'))
        return response.json['token']

As you can see, getting a token is quick. Here I'm taking advantage of the generic user "susan", which is automatically added to the database in the populate_db() method discussed earlier.

Testing Authenticated API Routes

The last test that I'm going to show you uses a token to make a request to get the complete list of users. You can see the implementation of this test below:

    def test_api_get_users(self):
        token = self.get_api_token()
        response = self.client.get(
            '/api/users', headers={'Authorization': f'Bearer {token}'})
        assert response.status_code == 200
        assert len(response.json['items']) == 1
        assert response.json['items'][0]['username'] == 'susan'

Once again, this test is short and quick. I first retrieve a token from the auxiliary method added above, and then send it as a bearer token when I make an API request. The test is getting the list of users in the system, which should be only one, our "susan" pre-populated user.

Web Application Testing Summary

I hope these examples give you some ideas about how to approach testing your own application.

With all the tests shown in this article, I was able to grow the coverage of Microblog from the initial 40% to 55%. I hope this gives you a realistic feel of how arduous writing unit tests often is. While 55% is a good amount of coverage, there are still plenty of areas to expand on. As I have showed in previous articles, once you start running out of ideas regarding what to test, a look at the "missing" column in the coverage report will indicate what areas you need to concentrate on.

For your reference, below you can see the complete test_microblog.py file. If you want to run this file, just add it to the Microblog project in the root folder and then run pytest as shown above.

import os
os.environ['DATABASE_URL'] = 'sqlite://'  # use an in-memory database for tests

import re
import unittest
from flask import current_app
from app import create_app, db
from app.models import User


class TestWebApp(unittest.TestCase):
    def setUp(self):
        self.app = create_app()
        self.app.config['WTF_CSRF_ENABLED'] = False  # no CSRF during tests
        self.appctx = self.app.app_context()
        self.appctx.push()
        db.create_all()
        self.populate_db()
        self.client = self.app.test_client()

    def tearDown(self):
        db.drop_all()
        self.appctx.pop()
        self.app = None
        self.appctx = None
        self.client = None

    def populate_db(self):
        user = User(username='susan', email='susan@example.com')
        user.set_password('foo')
        db.session.add(user)
        db.session.commit()

    def login(self):
        self.client.post('/auth/login', data={
            'username': 'susan',
            'password': 'foo',
        })

    def get_api_token(self):
        response = self.client.post('/api/tokens', auth=('susan', 'foo'))
        return response.json['token']

    def test_app(self):
        assert self.app is not None
        assert current_app == self.app

    def test_home_page_redirect(self):
        response = self.client.get('/', follow_redirects=True)
        assert response.status_code == 200
        assert response.request.path == '/auth/login'

    def test_registration_form(self):
        response = self.client.get('/auth/register')
        assert response.status_code == 200
        html = response.get_data(as_text=True)

        # make sure all the fields are included
        assert 'name="username"' in html
        assert 'name="email"' in html
        assert 'name="password"' in html
        assert 'name="password2"' in html
        assert 'name="submit"' in html

    def test_register_user(self):
        response = self.client.post('/auth/register', data={
            'username': 'alice',
            'email': 'alice@example.com',
            'password': 'foo',
            'password2': 'foo',
        }, follow_redirects=True)
        assert response.status_code == 200
        assert response.request.path == '/auth/login' # redirected to login

        # login with new user
        response = self.client.post('/auth/login', data={
            'username': 'alice',
            'password': 'foo',
        }, follow_redirects=True)
        assert response.status_code == 200
        html = response.get_data(as_text=True)
        assert 'Hi, alice!' in html

    def test_register_user_mismatched_passwords(self):
        response = self.client.post('/auth/register', data={
            'username': 'alice',
            'email': 'alice@example.com',
            'password': 'foo',
            'password2': 'bar',
        })
        assert response.status_code == 200
        html = response.get_data(as_text=True)
        assert 'Field must be equal to password.' in html

    def test_write_post(self):
        self.login()
        response = self.client.post('/', data={'post': 'Hello, world!'},
                                    follow_redirects=True)
        assert response.status_code == 200
        html = response.get_data(as_text=True)
        assert 'Your post is now live!' in html
        assert 'Hello, world!' in html
        assert re.search(r'<span class="user_popup">\s*'
                         r'<a href="/user/susan">\s*'
                         r'susan\s*</a>\s*</span>\s*said', html) is not None

    def test_api_register_user(self):
        response = self.client.post('/api/users', json={
            'username': 'bob',
            'email': 'bob@example.com',
            'password': 'bar'
        })
        assert response.status_code == 201

        # make sure the user is in the database
        user = User.query.filter_by(username='bob').first()
        assert user is not None
        assert user.email == 'bob@example.com'

    def test_api_get_users(self):
        token = self.get_api_token()
        response = self.client.get(
            '/api/users', headers={'Authorization': f'Bearer {token}'})
        assert response.status_code == 200
        assert len(response.json['items']) == 1
        assert response.json['items'][0]['username'] == 'susan'
Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

13 comments
  • #1 Michael said

    Hi Miguel,
    Thanks for a great post!

    I have a problem writing tests to forms with Boolean Fields.
    In particular, tests with 'False' value, e.g. data={'some_field': False, ...}.

    After a lot of debugging with internal flask's code, it turns out that if I want to pass the 'False' value, I should use the empty string! i.e. data={'some_field': '', ...}
    Any other value is interpreted as True!
    I tried False (as a boolean), 'False' (as a string), etc., but nothing worked out.

    So... this my hacky method for solving this issue, and my question is: do you have any clue why? I mean, if its a bug, or some conflict with flask\wtf_forms... packages, or, this is just the way it should be (the way python interpret the data to js code later)?

  • #2 Miguel Grinberg said

    @Michael: this isn't a bug. The HTTP protocol does not have types, all the client can send in a form submission is strings. The empty string (or the absence of the field) is interpreted as a false value, while any non-empty value is interpreted as true.

  • #3 Liz said

    Thanks so much for this tutorial. I'm finding developing tests much harder than developing the application they are meant to test. Most tutorials on this topic are too basic. You have extended and clarified things for a real web app, like forms and HTML responses, which really helps. Still, for my app it fails because it really needs a Postgresql database rather than a MySql database. I'm looking into pytest-flask-sqlalchemy, which fits my requirements, but I am new to testing and am a little lost how it all fits together. Any suggestions?

  • #4 Miguel Grinberg said

    @Liz: I agree 100%, writing tests is hard and tedious. I find the initial push to achieve some level of testing coverage extremely difficult, but I find maintaining an already built test suite as new changes are added a lot easier.

    You can run the tests under any database supported by SQLAlchemy, without having to install extra packages. Create a testing database in your Postgres server, then set the DATABASE_URL variable to it in your unit tests and that should do it. There is nothing wrong with using a Postgres database for your unit tests, it's just a bit slower than using SQLite.

  • #5 Shahrukh said

    While typing the code in VS Code, it autofilled the entire method and added 'return super().setUp()' - what does this code do?

  • #6 Miguel Grinberg said

    @Shahrukh: your generated code is calling the setUp method in the parent class, but in this case this isn't necessary. It wouldn't hurt to do this, but in practice there is no need, because there is nothing that needs to be done by the parent class in this case.

  • #7 Anthony said

    Hi, thanks for the post Miguel! All your content is so useful.
    Just a note for those who might not quite follow these two lines in the setUp, which I had to look into:
    self.appctx = self.app.app_context()
    self.appctx.push()
    Miguel discusses this in an older video at ~0:43min:
    https://blog.miguelgrinberg.com/post/flask-webcast-2-request-and-application-contexts
    My attempt at a summary: It's an alternative syntax to app.app_context() as context manager. Both install an application context on a thread. Probably better to watch his other video!!

  • #8 Oliver said

    Thanks for your blog :-)

  • #9 Oliver said

    If I do a db.drop_all() at teardown(self): no queries in the tests work for me anymore (Posgresql). But why ?

  • #10 Miguel Grinberg said

    @Oliver: Your tests don't work how? Do you create the tables again in the setUp method?

  • #11 kris said

    I am working through your mega flask blog tutorial, and finding that when running both the above tests and the ones in the original blog, the main database tables (but not the database, app.db, get deleted each time;

  • #12 Miguel Grinberg said

    @kris: That is how it should work. The tables are deleted and recreated for each test run. If it bothers you to have the database file on disk you can delete it as well, but that is not necessary, and also would not be possible for other databases beyond SQLite.

  • #13 Yuming said

    Thank you very much for this tutorial, unittest is absolutely necessary if we plan to publish web applications with advanced features. I have learned a lot from this series and covered quite some functionalities of my apps. Your tutorial about using VSCode for unittest debugging (at https://www.youtube.com/watch?v=UXqiVe6h3lA) is also very helpful.

Leave a Comment