the red penguin
HOME ABOUT SITEMAP BLOG LOGIN

38. Writing API tests

38.01 Simple test

Let’s start with a simple test, then we’ll explain how to test our API.

In tests.py we need to import Python’s JSON, TestCase and Reverse:

import json
from django.test import TestCase
from django.urls import reverse
from django.urls import reverse_lazy

Reverse allows us to take a path in our URL’s file and turn it into an actual URL string. We’ll use that a lot in our tests.

from rest_framework.test import APIRequestFactory
from rest_framework.test import APITestCase

These classes from the rest_framework allow us to test the rest framework more easily.

We also need to import our model factories so we can use them, and also our serializers so we can test them later.

from .model_factories import *
from .serializers import *

We now need to write our first test class.

We need to write our function with any name starting with a keyword of test. Let’s give them useful names, eg test_geneDetailReturnsSuccess.

We then want to populate the database with some dummy data, and we’ll use our GeneFactory to do that.

I’m going to override some of the defaults. If we look in our model factory, we’ve set various defaults and you’re free to override these when you come to write the test. I’m going to override a couple. I’m going to make sure the primary key is always one, and that the gene Id is gene1 to go with that. Now I need to know what URL I’m sending this to.

We’re giving url a name of gene_api, as this test requires that URLs has names. This essentially takes the URL and this information, and constructs the actual URL string that the user agent would use.

We then need to submit it to our application using response. We have the client class that can call get on our application, given the URL we just constructed, and then we’ll return some response. This is like a little dummy user agent.

We then need to render the response to get the data out. This would allow you to get access to the HTML page, or access to the JSON document.

Then we actually need to test something. We’re going to assert that something is equal to something else. For the response, the HTTP status code should be 200.

class GeneTest(APITestCase):

    def test_geneDetailReturnsSuccess(self):
        gene = GeneFactory.create(pk=1, gene_id="gene1")
        url = reverse('gene_api', kwargs={'pk': 1})
        response = self.client.get(url)
        response.render()
        self.assertEqual(response.status_code, 200)

We mentioned that our URLs need to have names, and we need to add these in urls.py too. So we need to go to urls.py and add names. We’ve just definted gene_api as a name for the gene detail, and we should add the name genes_api for the GeneList too as we mentioned that earlier.

    path('api/genes', views.GeneList.as_view(),  name='genes_api'),
    path('api/gene//', api.GeneDetail.as_view(), name='gene_api'),

Now to run the tests you need to ensure that you have permissions to create a new (temporary) database for testing. I did this by going to psql shell, logging in with the default user I set up (postgres) and my password (…), and then doing this for all users:

ALTER USER postgres CREATEDB
ALTER USER coder CREATEDB

Now all our users can create databases. So we can now run the test by opening the terminal window and instead of running the server, doing this:

(env) $ python manage.py test

This then creates a copy of the database for just putting test data in. As each test runs, it can use this copy of the database that will be destroyed at the end.

We can then see if the test successfully works or not.

38.02 More advanced test

Let’s start building up some more tests that are more useful for our API. We know that our API can return things successfully, but let’s test what happens if a user sends something that’s malformatted.

In our previous class we need to add a second function like this:

    def test_geneDetailReturnFailOnBadPk(self):
        gene = GeneFactory.create(pk=2, gene_id="gene2")
        url = "/api/gene/H/"
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

If the user sends a bad gene identifier or a bad primary key, we should send the correct error response back to the user.

This is a second gene and we’re just going to call it gene2. Let’s say the user tries to access gene2 using an incorrect URL such as /api/gene/H/.

When the client calls that URL, we’re expecting an integer to be in the place at the end of the URL, but instead, we’ve got this letter H. So when we send a response, we’re going to check that we correctly turn a 404 response, because no such thing is here. All of our genes have integer values.

When we run the tests again, we can see that 2 tests have been run correctly.

We can refactor our class by making use of some default functions to setup and tear down, and some class variables. I’m going to make a couple of variables for genes. One for a valid URL and one for a bad URL. Then we can use the setup function to define what these class variables are. Then we can reuse these class variables to write our tests, and then we can make our tests a little easier to read.

We can add our class variables and set up a SetUp function to use these:

    gene1 = None
    gene2 = None
    good_url = ''
    bad_url = ''

    def setUp(self):
        self.gene1 = GeneFactory.create(pk=1, gene_id="gene1")
        self.gene2 = GeneFactory.create(pk=2, gene_id="gene2")
        self.good_url = reverse('gene_api', kwargs={'pk': 1})
        self.bad_url = "/api/gene/H/"

At the end of our test, I want to return the database to the state it was in before the test class started. So if I write more test classes, I can be confident that the database is clear and clear to use.

    def tearDown(self):
        EC.objects.all().delete()
        Sequencing.objects.all().delete()
        Gene.objects.all().delete()
        ECFactory.reset_sequence(0)
        SequencingFactory.reset_sequence(0)
        GeneFactory.reset_sequence(0)

Essentially, for each of our tables that we have factories for, I’m just going to delete the contents.

Also, just in case it’s an issue, I’m also going to reset the primary keys. The primary keys and auto-increment value on the database. We can just use this factory function to just reset that, back to the initial value on the database.

Now we can turn our attention to our functions and we can refactor them to start using our class variables.

    def test_geneDetailReturnsSuccess(self):
        response = self.client.get(self.good_url, format='json')
        response.render()
        self.assertEqual(response.status_code, 200)

    def test_geneDetailReturnFailOnBadPk(self):
        response = self.client.get(self.bad_url, format='json')
        self.assertEqual(response.status_code, 404)

We’ve not tested very many things so maybe we want to also check that the data that’s arrived is correct.

        data = json.loads(response.content)
        self.assertTrue('entity' in data)
        self.assertEqual(data['entity'], 'Plasmid')

The response object has the content, so this is the actual JSON string and we use Json.loads to load it into a Python data structure. Then we can have some new asserts. An assert that it’s true, that the Python data structure contains a key called entity. Then also, assert for that key contains the value plasmid, so this is the value set in our factory. Now we can run our tests once again.

Okay. Both our tests are still passing, so all those assertions are still true. Not only is the object coming from the database, the correct HTTP response with a status code of 200. But we’re also getting the data that we expect, the data that’s from our fixtures.

We probably also want the test that deleting records works. Let me scroll up to our class variables, going to add a delete URL type.

    delete_url = ''

Then in my setup function, I’m going to add a new gene, gene number 3, primary key of 3, ID 3.

        self.gene3 = GeneFactory.create(pk=3, gene_id="gene3")

Then I’m going to add a URL for deleting this.

        self.delete_url = reverse('gene_api', kwargs={'pk': 3})

Then I can use that information to write some nice and small tests for the deletion.

    def test_geneDetailDeleteIsSuccessful(self):
        response = self.client.delete(self.delete_url, format='json')
        self.assertEqual(response.status_code, 204)

Now we can run our test again. Those all passed, so we’ve now got three tests that cover some of the functionality for our gene detail.

Wednesday 1 December 2021, 440 views


Leave a Reply

Your email address will not be published. Required fields are marked *