Skip to content

Testing Django, Flask applications

Vladimir Turov edited this page Feb 6, 2021 · 10 revisions

In brief

In hs-test-python testing library there is a possibility to test backend applications based on Flask and Django libraries. The testing process somewhat similar, so this page covers both frameworks.

Extend class

First of all, you need to extend an appropriate class. In case of Django, you need to extend DjangoTest, and in case of Flask you need to extend FlaskTest.

Import an appropriate class from the hstest module:

from hstest import DjangoTest
from hstest import FlaskTest

And then extend an appropriate class:

class ServerTest(FlaskTest):
   ...

class ServerTest(DjangoTest):
   ...

Define the source

Inside the body of the class, you can define source - the path to file you want to execute.

With Django it's easy, the hs-test-python library will automatically find manage.py across all the user's files and run this file with arguments runserver PORT --noreload. So you don't need to set source.

With Flask the file to execute can have any generic name, so you are forced to set the source field.

The source field can be of type str or tuple or list. You should set the module name, not the path to the file. For example, if the file name is server.py then the module would be server. Another case is when the file path is module1/client.py then the module name would be module1.client.

class ServerTest(FlaskTest):
    source = 'module1.client'
    ...

In this case, a free port will be found automatically in the range 8000-8099 inclusive. But you can define the port specifically as a second item of the tuple. Notice that if you don't write parentheses the Python would understand that as a tuple.

Examples:

source = 'module1.client', 5001
source = ('module1.client', 5001)

When extending FlaskTest you can also set multiple sources, meaning multiple Flask applications will be available when testing. You can set all the sources in the list. Every item on the list should be either str or tuple. DjangoTest supports a single execution source at the moment.

Examples:

source = ['module1.client']
source = ['module1.client', 'server']
source = [('module1.client', 5001)]
source = [('module1.client', 5001), ('server', 5002)]
source = ['module1.client', ('server', 5002)]
source = [('module1.client', 5001), 'server']

Note: in Django and Flask host and port are sent to the user's program via command-line arguments. Django manages them automatically in manage.py autogenerated file but Flask doesn't. The host and port are sent to the Flask application via the 2nd command-line argument in the format host:port, for example localhost:8000. You need the following code to address that:

if __name__ == '__main__':
    if len(sys.argv) > 1:
        arg_host, arg_port = sys.argv[1].split(':')
        app.run(host=arg_host, port=arg_port)
    else:
        app.run()

The sample above can be run with or without command line arguments being passed. This way, users don't need to worry about host and port while testing their program by themselves. Please, include this sample in the initial template for the user in Hyperskill Flask projects.

Use database

When extending DjangoTest you can use a separate database for testing. For this, you need to set use_database = True.

class ServerTest(DjangoTest):
    use_database = True
    ...

The hs-test-python library

  1. Sets the environment variable HYPERSKILL_TEST_DATABASE to db.test.sqlite3
  2. Creates a file db.test.sqlite3. If it already exists then the library deletes it and creates an empty file.
  3. Runs python manage.py migrate command to set the database with the correct tables.

If you want to use another name of the database, you should set test_database field with the name of the database.

class ServerTest(DjangoTest):
    use_database = True
    test_database = 'another_name.db'
    ...

In the initial template in file settings.py you should use the following code:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.environ.get('HYPERSKILL_TEST_DATABASE') or os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

Create tests

You should use @dynamic_test decorator to write tests. See this article on how to write dynamic tests.

Use self.get(address) to make GET request to the source. If you have multiple sources, you should specify a source also, like in source field.

Example:

class ServerTest(DjangoTest):
    @dynamic_test
    def test1(self):
        hw = self.get('/')

        if hw != 'Hello, World!':
            return wrong(f'Django service should return "Hello, World!" '
                         f'performing "GET /", found "{hw}"')

        return correct()

Other example:

class ServerTest(FlaskTest):
    source = ['server', 'module1.client']

    @dynamic_test
    def test1(self):
        hw = self.get('/', source='module1.client')

        if hw != 'Hello, World Client!':
            return wrong(f'Flask client should return "Hello, World Client!" '
                         f'performing "GET /", found "{hw}"')

        return correct()

If you want to perform POST request, or you want to use requests library, you can use self.get_url(address) to get host and port and attached address as a string. By default, the attached address is an empty string.

Example:

import requests

class ServerTest(DjangoTest):
    @dynamic_test
    def test1(self):
        hw = requests.get(self.get_url('/home'))

        if hw != 'Home page':
            return wrong(f'Django service should return "Home page" '
                         f'performing "GET /home", found "{hw}"')

        return correct()

Another example:

import requests

class ServerTest(FlaskTest):
    source = ['server', 'module1.client']

    @dynamic_test
    def test1(self):
        hw = requests.get(self.get_url('/home', source='server'))

        if hw != 'Home Server':
            return wrong(f'Flask server should return "Home Server" '
                         f'performing "GET /home", found "{hw}"')

        return correct()

Reuse tests

Sometimes you want to use the same file for every stage of the project containing all the tests for all the stages. You can use a separate class for that, but that class should be located in the test folder. To import it correctly, you should also create an empty __init__.py in the test folder. So, the file hierarchy should look like the following:

stage1
    manage.py
    ...
    test
        __init__.py
        base.py
    tests.py
stage2
    test
        __init__.py
        base.py
    tests.py
...

base.py might look like the following. Notice that before each method there is no @dynamic_test decorator.

from hstest import DjangoTest

class BaseServerTest(DjangoTest):
    use_database = True

    def test1(self):
        ...

    def test2(self):
        ...

    def test3(self):
        ...

    ...

And you can use data parameter to quickly chose tests you need for the specified stage. Here's tests.py example:

from test.base import BaseServerTest

class Stage1Test(BaseServerTest):

    funcs = [
        BaseServerTest.test1,
        BaseServerTest.test3,
        BaseServerTest.test5,
        ...
    ]

    @dynamic_test(data=funcs)
    def test(self, func):
        return func(self)


if __name__ == '__main__':
    Stage1Test().run_tests()