Skip to content

Adding a test

Django

Basic Example

Our way of testing is not too different to the way you would normally write tests in Django However there are some differences due to the way we use Django's multi-db support.

from base.test import BaseTestCase


class HelloWorldTest(BaseTestCase):

    def test__addition__one_plus_one__equals_two(self):
        self.assertEqual(1 + 1, 2)

In general, any test that needs to operate on the database should extend from base.test.BaseTestCase. This class ensures each database is configured correctly, with the default data we would expect from a new account creation etc.

Each test name should follow the test naming scheme

Using client models

You can access any client model as normal, there is a single client setup by default.

Accessing client connection

The test client's connection name is configured using settings.TEST_CLIENT_CONNECTION_NAME. You should use this if you ever need access to the raw connection.

from django.db import connections
from django.conf import settings

with connections[settings.TEST_CLIENT_CONNECTION_NAME].cursor() as cursor:
    cursor.execute('SELECT 1', (,))

Access Account instance

The test client's account token is configured using settings.TEST_CLIENT_TOKEN. You should use this if you ever need to retrieve an Account instance.

from django.conf import settings
from base.models import Account

Account.objects.get(token=settings.TEST_CLIENT_TOKEN)

Running tests

You can run tests as you normal would in Django, using the test management command.

sudo python3 manage.py test

Note

In development, you likely want to use the --keepdb flag to prevent the test database from being destroyed each time you run test.

sudo python3 manage.py test --keepdb

Generalised testing concepts

When testing an API, we want to see that it is doing what we expect when we hit certain endpoints. The easiest way to find out what endpoint to test is to use the network tab to see the network request that is made when you change a resource using the frontend.

From there, we want to test that the functions of the API work as we expect. In the example test in test_account_config.py, we test three functions of the api/config/event endpoint - create, modify and delete.

For each function of the API, we write a separate test function. These should be named descriptively, as when a test errors or fails the default test output will include the test name. Test functions should always start with test_ - Django’s manage.py test command looks for functions with this prefix within test classes, and will not run your test if it is not prefixed with test_. See here for the way we want to name tests.

Within each function, we need to understand a) how the data sits at rest in our database, b) how Django interacts with that data via models, c) what parameters we need to pass to our endpoint to make a valid request and d) what effect that should have had on the data in a) and b).

Your tests should be comprehensive without being overly verbose - for example if you are creating a new model instance you don’t need to worry about any other models.

Auth mixin

Our API endpoints require authorisation (a logged in user) for access. This is abstracted away from the test classes themselves in the APIAuthMixin. All you need to do to have a viable, logged in user is call the create_accounts_and_user function in your test class.

The test client

The test client is the mock browser client that will be making requests to our endpoints. This client is already prepared for you and sits in the APIAuthMixin. You can access it with self.client.

response.status_code

Below is a list of HTTP status codes that might be useful - I tend to test the status code of the response in every test.

Status codes

--keepdb

Django tests work by spinning up a test database from the project’s model history, which is destroyed when the test run is finished. Our database structure is sprawling, so creating this database takes time. This means that your test run will also take time - which will be very frustrating. You can pass the argument --keepdb when running your tests, which will use your existing test database (devname_attrib_client__$test) if available and create it if not. Crucially, it will also preserve your test database instead of destroying it when the test run ends.

Note that if there are migrations in development that you do not have locally, the first time you run your tests you should run them without --keepdb so that your test database is created with the latest model structure.

setUp()

Test classes should extend BaseTestCase. Any test class that extends BaseTestCase implicitly calls the setUp() function. Note the spelling of setUp, it is not all lowercase. It is often helpful to override the setUp() function when testing so you can add the data you need to the database before testing. Note that test classes and functions are strictly scoped, so data you create in MyTestClass.setUp() will not be available in MyOtherTestClass.

Testing example walkthrough

As you can see from the example in test_account_config.py, we first set up my test environment by adding an Event to the database manually using the Django ORM. Then, we create three test functions each starting with test_. Within each, we make an appropriate request to our chosen endpoint using our test client (self.client). Here, we have to use the appropriate HTTP verb(POST, PATCH, DELETE, etc) to ensure that my request headers are set correctly. You will also find it necessary to specify a content-type header, which is usually application/json, but others do exist; especially when downloading CSVs.

Once we have our response, we test that the request has done what we are expecting. It is good practice to assert the response.status_code is as expected - 201 where a new object is created, for example - as our first test, because this will fail fast if any changes are made to the API in future.

Then, we confirm that the object we are testing exists in the state that we are expecting. We do this using assertions - we assert that (eg) the name of the Event object we just created is the same as the one we passed to the API. Or, we might test that the name has changed. Or that the Event has been set to active=False when a delete request is made, as our Events are configured not to be deleted as standard.