• Portretfoto van Kees Hink
    Kees Hink

Testing Django Applications (3)

Everyone agrees that automated tests are a good thing, but getting started can be a puzzle. In these blog posts we give practical advice, full of code examples, on how to write high quality tests for Django. This is part 3.

Introduction

In 2024 Coen and I turned our PyGrunn talk into a 2-part blog post about testing Django applications. We mentioned that we omitted Continuous Integration, testing Javascript and testing Django+HTMX. We'll talk about these and other subjects now.

Changes since 2024

Looking back on our previous two blog posts, we wonder: has anything changed? Are there new tools everyone should know about? Have there been new insights causing us to do things differently?

Using LLM's

One thing that we started doing more is using Large Language Models to write tests. It seems that many models have been trained on Django tests extensively, so the suggestions are often quite good. You'll still have to check of course. For example, when starting with def test_my_async_task, LLM's will often mock away the actual task, instead of running it. But when the assignment is understood, LLM's can speed up the work and make good suggestions you might not have thought of yourself.

But really, not much changed

The previous blog posts are still perfectly valid. This is largely thanks to Django being such a robust framework. What works today will probably still work next year, with only minor changes.

Making tests part of the development process

Continuous integration can be about a lot of things. In the context of this blog post, we will focus on the aspect of running tests automatically.

Because it's no good writing tests and never running them. Tests serve a purpose: When a change breaks existing behavior, we want to know it. Therefor, you should not rely on developers (even yourself) running tests themselves. Django projects are no different from other software projects in this respect, but if you're reading this, chances are you're not yet familiar with using CI tools.

Most people use Github, Gitlab or Bitbucket to create Pull Requests or Merge requests. These platforms make it easy to run the tests on each PR/MR, and require them to pass before the code can be merged. This is the perfect time for it: Before the code gets merged.

Merge Request in Gitlab with failed tests
Merge Request in Gitlab with failed tests. The "merge" button won't merge now!

Presenting examples or sharing our setup here would only be confusing, especially as this is already very well documented (if a bit overwhelming at first).

Even if you don't use these platforms, you can set up something to run tests automatically (Jenkins comes to mind).

Testing Javascript

If your project contains Javascript, you can't use any of the testing methods we supplied before. Neither the DjangoTestClient nor django-webtests's client has the capacity to run Javascript. The best you can do is test that some Javascript is loaded from a specific file.

In the JavaScript ecosystem, unit and component testing are usually handled by tools like Jest or React Testing Library.

But if you'd like to test sprinkles of Javascript in a Django application, you can create end to end tests. Tools like Playwright and Selenium open the application in a browser window. They can test using many different browsers (Chrome, Firefox, Edge, Safari).

You might wonder why we don't test everything in this way. Some reasons are:

  • it's a lot slower to start up the entire Django server and a browser to run a few tests
  • these tests require more work to write and maintain
  • when a test fails due to backend code, it's not clear what part of that code caused it

If you're interested in the different kinds of tests, and what to use when, search for "test pyramid", or check out this Dutch article by Coen.

Testing Django+HTMX

HTMX is a way to add interactivity to a page without writing a lot of Javascript. However, HTMX itself is Javascript. As such, it would require and end-to-end testing framework like Playwright to test it properly.

However, you can also test the HTMX responses of your Django framework without all that.

Probably, you have a place in your view where you define how to handle HTMX requests, which are distinguishable by the "HX-Request" header. django-htmx makes this available in the Request.htmx property.

So this could be an exampe for a view that only works on HTMX requests:

class UserUpdateView(UpdateView):
    model = User
    form_class = UserForm
    ...

    def form_valid(self, form):
        if not self.request.htmx:
            # Non-HTMX requests are not allowed
            return self.form_invalid(form)

        # Update user
        ...

To test this code path, you can include the "HX-Request" header when submitting a form:

def test_user_update_view_edit_htmx(django_app):
    user = UserFactory(first_name="Bruce")
    response = django_app.get(reverse("website:user-update"), user=user)
    form = response.forms["user-update-form"]
    form["first_name"] = "Caitlyn"

    response = form.submit(headers={"HX-Request": "true"})
    user.refresh_from_db()
    assert user.first_name == "Caitlyn"

Pytest tricks

The commands pytest --markers and pytest --fixtures will give you all available markers and fixtures. Check them out and get inspired!

Conclusion

Not much has changed since last year. Although new technologies (like LLMs) enter, the basics of Django testing are the same. End-to-end testing can be valuable, but also cumbersome. The goal of testing is to guarantee quality and save you headaches, so use end-to-end tests sparingly.

We love code