Primary image for Rescuing Django Projects with Smoke Tests: Quick Wins for Long-Term Success

Rescuing Django Projects with Smoke Tests: Quick Wins for Long-Term Success

We often inherit existing Django projects at Lincoln Loop either to provide ongoing maintenance or build new features (or both). Usually these projects are in some state of neglect and disrepair when they come to us. If they were a house, they might be on the verge of being condemned and require some urgent repairs to prevent a catastrophe.

To make matters worse, the tests are usually non-existent or broken to start with. So how do you make changes and have some level of confidence that you aren’t breaking other things in the process?

Writing a complete test suite from scratch would be a large (expensive) undertaking and one that would not deliver much in terms of immediate value. We use smoke tests.

Smoke tests give you the maximum coverage with the minimum effort. Pick out the key views or API endpoints and test that they return a 200 status code. Got something that requires a login? Verify it returns a 302, 401, or similar.

Here are a few examples of smoke tests in Django (also using the wonderful model-bakery library):

from http import HTTPStatus

from django.contrib.auth import get_user_model
from django.tests import TransactionTestCase
from django.urls import reverse
from model_bakery import baker

class SmokeTests(TransactionTestCase):
    def test_homepage(self):
        """Verify home page returns 200 status code"""
        response = self.client.get(reverse("home"))
        self.assertEqual(response.status_code, HTTPStatus.OK)

    def test_api_anon(self):
        """Verify API requires login"""
        response = self.client.get(reverse("api:get_items"))
        self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED)

    def test_api_authed(self):
        """Verify a logged-in user can access the API"""
        user = baker.prepare(get_user_model())
        self.client.force_login(user)
        response = self.client.get(reverse("api:get_items"))
        self.assertEqual(response.status_code, HTTPStatus.OK)

Are these tests perfect? Definitely not. Lots can go wrong that these tests won’t catch.

Are they better than nothing? Absolutely! They will exercise large swaths of code and will catch show-stopper bugs that otherwise might go undetected.

With smoke tests we can move a little more confidently in a foreign codebase, allowing us to fix the most egregious problems

What’s Next

We want to work on improving tests and coverage over time while still delivering immediate business value to the client throughout the process.

We need to know if our fledgling tests are passing (and not just on the developer’s laptop), so if there’s no CI, we will get something basic going there. Usually in GitHub Actions or AppPack.

Then we start working with a “leave it better than you found it” philosophy. Need to fix a bug in an untested portion of the code? Deliver it with a test. Writing a new feature? Deliver it with a test.

Over time, both the quality and coverage of the test suite will improve and you’ll be able to rescue the project from condemnation.

Peter Baumgartner

About the author

Peter Baumgartner

Peter is the founder of Lincoln Loop, having built it up from a small freelance operation in 2007 to what it is today. He is constantly learning and is well-versed in many technical disciplines including devops, …