A minimal Django testing style guide
Tags: django, python, testing
Published: 2021-10-01 15:15:42

You started on a [MVP](https://en.wikipedia.org/wiki/Minimum_viable_product). Usage is increasing. The occasional bug.

More users. Suddenly this MVP is not an MVP anymore. A core business process depends on this system your team is maintaining.

You ship a user-reported bug fix. Users report another two. Sounds familiar?

Since it was an MVP, you paid little attention to writing automated tests.

What is the current codebase's test coverage? Wait. Do you even measure it?

This post is not about setting the gold standard on how to write tests for your Django application.

It's about setting a simple direction as possible. And getting started. Wherever you are.

In this I also an attempt to document some of my practices around writing tests for Django projects.

## Terminology

As in this [Python testing style guide](https://blog.thea.codes/my-python-testing-style-guide/) I followed in the past:

> I do not make a distinction between **unit tests** and **integration tests**. I generally refer to these types of tests as unit tests. I do not go out of my way to isolate a single unit of code and test it without consideration of the rest of the project. I do try to isolate a single unit of *behaviour*.

## Where do I place tests?

The greatest advantage of having tests is that they are *living docs* about your code. The inputs/outputs tested either work as expected or they don't. They cannot go outdated. Unlike written docs.

So it's important to have an obvious way to tell where the test for a piece of code resides in your codebase.

pytest's [conventions for Python test discovery](https://docs.pytest.org/en/latest/explanation/goodpractices.html#conventions-for-python-test-discovery) are simple to agree on and follow.

Let's say you have an `Article` model in app `articles` at `articles/models.py`:

```python
class Article(models.Model):
    ...
```

Tests for this model class would go in `articles/tests/test_models.py`:

```python
class ArticleTest(TestCase):
    ...
```

In a similar fashion, if we have a function in `articles/utils.py`:

```python
def do_something():
    ...
```

its tests would go in `articles/tests/test_utils.py`:

```python
class DoSomethingTest(TestCase):
    def test_do_something(self):
        ...
```

Why create a `DoSomethingTest` extending a `TestCase`? Rather than a standalone test function?

The default way to write tests in Django is using Python's built-in [unittest module](https://docs.python.org/3/library/unittest.html). This defines tests using a class-based approach.

`django.test.TestCase` is in fact a subclass of `unittest.TestCase` that runs each test inside a transaction to provide database-oriented goodies. Such as transaction isolation. Django's [writing and running tests](https://docs.djangoproject.com/en/dev/topics/testing/overview/) docs provide more details on this.

### Naming test classes

As in the case of "placing" a test in your codebase, a `TestCase`'s name should be indicative of the class (or standalone function) being tested. So for classes:

```python
class Article(model.model)
    ...

class ArticleTest(TestCase)
    ...
```

and for functions:

```python
def do_something():

...

class DoSomethingTest(TestCase):
    def test_do_something(self):
        ...
```

### Naming test functions

As mentioned above, your individual tests should test for a *single unit of behaviour*. In the below case, behaviour depends on the data (state) the logic depends on:

```python
class Article(models.Model):
    title = models.CharField(max_length=32)
    slug = models.CharField(max_length=32, unique=True)
    content = models.TextField()

    def __str__(self):
        return self.title

    def get_short_title(self):
        if len(self.title) > 15:
            return self.title[: (15 - 3)] + "..."
        return self.title
```

So we have to test `get_short_title` based on the `title`'s length. The more "explicit" the name the better. Have test function names that reflect the state relevant for the expected result:

```python
class ArticleTest(TestCase):
    def test_get_short_title_title_is_longer_than_15_characters_yes(self):
        ...

    def test_get_short_title_title_is_longer_than_15_characters_no(self):
        ...
```

Notice how the whole class name to function name structure tells us:

* name of class `Article`
* name of function `get_short_title`
* state (input) determining behaviour (output) per test

This also makes obvious what should be changed in case you move/rename classes/functions. Drawn out it looks like this:

![Model code tests and layout](https://untangled.dev/assets/img/posts/2021-08-22-dj-testing-styleguide/model-code-and-tests-layout.png)url="" %}

Below is a screenshot of my setup. With code on the left, and tests code on the right. No fancy screen required. I took this on a laptop screen:

![Code and tests in Visual Studio Code](https://untangled.dev/assets/img/posts/2021-08-22-dj-testing-styleguide/model-code-and-tests-layout.png)

I find this a game changer when reading code. Seeing a function's inputs and expected outputs next to the function's code speeds things up. _A lot._

## Creating test data

Use [factory](https://en.wikipedia.org/wiki/Factory_(object-oriented_programming)) libraries to standardise test data creation.

The most popular two in Django are [factory-boy](https://factoryboy.readthedocs.io/en/stable/index.html) and [model bakery](https://model-bakery.readthedocs.io/en/latest/). The latter is simpler, so we use it in this article's examples. But the former is more feature-rich.

As a guiding principle keep your factory code *light*. I.e. specify only the fields that need to be set for the state required by the test.

For example, when creating the `article` to test `__str__`:

```python
article = baker.make("articles.Article", title="Test Title")
```

we are not passing `slug` or `description`. These would be noise for our test's purpose.

The way we create data via factories should be about the data state expected by the test. No more, no less.

*Why not just use fixtures?* You may already have [fixtures](https://docs.djangoproject.com/en/dev/howto/initial-data/#providing-data-with-fixtures), with a lot of data as expected in production, lying around.

Fixtures are an overkill for tests because:

* Maintaining them is hell. For example, what if you rename a model field?
* Maintaining fixtures is usually append-only. So the fixture's data increases over time, loading a lot of data the test doesn't need, *every time* the test is run.
* Fixtures make it hard to tell what data is being loaded. It's even *harder* to tell why a fixture is loading the data it is loading in the context of tests!

I find eliminating fixtures in tests is one of the most effective steps to speed up a test suite's runtime.

Not convinced? Section `11.1` in [Speed up your Django Tests]((https://gumroad.com/a/233305203)) delves into more detail why fixture files should just be avoided.

## Testing views

The above basic examples were all about testing models. Models are the usual starting point in writing a Django app.

But a lot of logic resides in views. And the forms and/or serializers that go with them.

### Unit vs integration, again

Django and DRF provide the [test client](https://docs.djangoproject.com/en/dev/topics/testing/tools/#the-test-client) and [APIClient](https://www.django-rest-framework.org/api-guide/testing/#apiclient) to make HTTP calls against your application programmatically.

The problem with making full HTTP calls against your application is that not only your view code wiill run. HTTP requests pass through the whole Django [middleware](https://docs.djangoproject.com/en/3.2/topics/http/middleware/) machinery, including, for example, authentication.

So when using the test client, you would be doing more of an integration test, rather than a unit test.

The alternative would be instantiating views. And passing instances of [`RequestFactory`](https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.RequestFactory), or [`APIRequestFactory`](https://www.django-rest-framework.org/api-guide/testing/#apirequestfactory) in DRF, directly to the view functions. Same approach applies for serializer (or form) functions.

Which way do you go? Ask yourself a couple of questions:

1. Is your current test suite running slow?
2. Do you have a lot of added middleware a request needs to go through?

If both answers are "no" (especially if you have no tests!) then the test client is likely enough to start with.

The upside of making HTTP requests is taht you are testing the whole URL/view binding. Rather than a standalone class-based view function like `get_queryset`.

Let's say a FE engineer on your team asks you a question about how a particular URL is supposed to work. Having tests with the test client allows you to be 100% sure of what the view at that URL is supposed to do. This is an example of what I meant above by *living docs*.

### How to structure your view tests

When using the test client I find it helpful to think of my application as a "black box":

* Assuming there is this data set up (inputs)
* What should happen if I submit this request to this URL? (action)
* Assert response returned (output)

The pattern is the one when writing tests for functions. But in this case rather than calling the specific function under test, the test makes an HTTP request.

Let's say we want CRUD views for our `Article` model. This is what we'd find in `articles/serializers.py`:

```python
class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = "__all__"
```

And in `articles/views.py`

```python
class ArticleListCreateAPIView(generics.ListCreateAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer


class ArticleDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
```

These are the tests we'd need to write to assert the API *behaves* as expected:

![Views code and tests layout](https://www.untangled.dev/assets/img/posts/2021-08-22-dj-testing-styleguide/views-code-and-tests-layout.png)

We do not need to write tests for the serializer, as it is used by the views.

### Testing configuration

In many cases, as shown above, and especially when using [class-based views](https://docs.djangoproject.com/en/dev/topics/class-based-views/), functionality is achieved through configuration of classes rather than coding. [^1]

[^1]: Aka [convention over configuration](https://en.wikipedia.org/wiki/Convention_over_configuration).

How to test in this case? The quickest way, i.e. the  *minimal* in terms of *time*, is to test using the client.

Let's extend our view to be able to order articles by `title`. The easiest option would be to extend `ArticleListCreateAPIViewTest.test_list` to:

1. create test data with different `title`s to be able to assert results order
2. call the URL with `ordering` parameter
3. test outcome

View code:

```python
class ArticleListCreateAPIView(generics.ListCreateAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filter_backends = [filters.OrderingFilter]
    ordering_fields = ["title"]
```

Test code:

```python
class ArticleListCreateAPIViewTest(APITestCase):
    def test_list(self):
        article1 = baker.make("articles.Article", title="Title A")
        article2 = baker.make("articles.Article", title="Title C")
        article3 = baker.make("articles.Article", title="Title B")

        # default ordering by id:
        response = self.client.get("/api/articles/")
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(
            [article["id"] for article in response.data],
            [article1.id, article2.id, article3.id]
        )

        # ordering by title:
        response = self.client.get("/api/articles/?ordering=title")
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(
            [article["id"] for article in response.data],
            [article1.id, article3.id, article2.id]
        )
```

While testing "code owned by others" these tests ensure that the API provided works as expected.

## Diving deeper

This guide is intended to be minimal. As such it misses going into some important aspects of writing tests.

### Test runners other than `TestCase`

Django's `TestCase` extends `unittest`'s `TestCase`. And DRF's `APITestCase` extends Django's `TestCase`. This guide does not go into other test runners like `pytest`. Which take a more functional approach.

### Mocking

What if your code needs to read data from an external API? Or upload a file to S3?

If the test needs to interact with external systems, it would become a "system" test. A test that depends on components "outside" the local environment where it runs.

Your automated tests should _always_ **mock** such interactions.

Getting into mocking is out of this style guide's scope. The tool Python provides is [unittest.mock](https://docs.python.org/3/library/unittest.mock.html).

### Coverage

On its own, `coverage` is a "dumb" measure of how many lines in your codebase are "covered by" tests. Why dumb? You can write a bunch HTTP GET requests in your tests, write _zero_ assertions, and increase coverage _a lot_. That, of course, doesn't add any value.

So do _not_ write tests for coverage.

Rather use [coverage.py](https://coverage.readthedocs.io/en/latest/) to identify which lines, i.e. *behaviour*, your tests do not cover.

Call me naive, but I still strive for [100% coverage](https://adamj.eu/tech/2019/04/30/getting-a-django-application-to-100-percent-coverage/).

But how to get started?

In any project/team I work with, I fight for a non-negative diff coverage on every PR on CI. Diff coverage [definition](https://diff-cover.readthedocs.io/en/latest/README.html#overview):

> Diff coverage is the percentage of new or modified lines that are covered by tests. This provides a clear and achievable standard for code review: If you touch a line of code, that line should be covered. Code coverage is every developer’s responsibility!

Over time you'll get to 100% coverage 🚀

### Speeding tests up

In this guide we go over creating test data using factories as opposed to fixtures. But that's just it.

In the "views" section I mentioned avoiding rendering responses to only test the behaviour "under test".

This is scratching the surface. [Speed Up Your Django Tests](https://gumroad.com/a/233305203) by [Adam Johnson](https://adamj.eu/) is the best resource on this topic.

And not only. It is in fact a great resource when starting to write tests from scratch. The below chapters, in particular, are an excellent guide:
  * Chapter 10, *Test Structure*, on how to organise your tests' code.
  * Chatper 11, *Test Data*, on best practices in managing data for your tests, in the fastest way possible.
  * Chapter 12, *Targetted Mocking*, on how to mock behaviour the right way.

## References / Further Reading

* [unittest module](https://docs.python.org/3/library/unittest.html) - Python's standard library.
* [Testing in Django](https://docs.djangoproject.com/en/dev/topics/testing/) - Django docs.
* [My Python testing style guide](https://blog.thea.codes/my-python-testing-style-guide/) -  [Thea "Stargirl" Flowers](https://thea.codes/). Main inspiration for this article.
* [Python Style Guide](https://github.com/octoenergy/conventions/blob/master/python.md#testing) - [Octopus Energy](https://tech.octopus.energy/). This style guide is a gold mine for any Python project. As in my article's case, no need to treat it as dogma. But it provides insight into how a large organisation, with presumably a large codebase, handles its "style guide". Kudos for sharing!
* [Why and how I get 100% test coverage for my Django projects, and you should too](https://web.archive.org/web/20190113090920/https://www.mxsasha.eu/blog/2014/09/11/why-and-how-test-coverage-100/). [Sasha Romjin](https://www.mxsasha.eu/).
* [Speed Up Your Django Tests](https://gumroad.com/a/233305203) - [Adam Johnson](https://adamj.eu/). Can't recommend this book enough!