Running Django Tests in Github Actions

I’ve bought into Github Actions as a CI/CD and deployment workflow for Django.

But since Github Actions is still young there are not as many guides on setting using it with Django, especially for the testing step.

If you are just getting started using Github Actions with Django, I suggest reviewing Michael Herman’s recent epic, “Continuously Deploying Django to DigitalOcean with Docker and Github Actions.” The repo for Michael’s tutorial is here.

What about Testing?

Test Driven Development or TDD or really just writing tests as a matter of habit while programming allows you to build bigger things faster.

If you’ve been building for a while, you know this, and maybe you’d enjoy Brian Okken’s Test & Code podcast.

I realize many folks are migrating over to Pytest, but I have not yet. So, this example focuses on the normal Django TestCase. It also presumes your use of postgres.

Why not use SQLite instead of Postgres for CI/CD Testing?

When I first looked at setting this up, I was tempted to use SQLite instead. After all, it wouldn’t require spinning up a container!

However, if you have read Speed Up Your Django Tests by Adam Johnson, you’d know that it is not a good idea to swap in the use of SQLite for Postgres when running your tests.

Thus, we shall not be tempted to use SQLite even though it might be easier! Instead we will seek shortcuts to make use of postgres in your Github Actions CI for Django as-fast-as-possible.

Example Django Test Job for Github Actions using Postgres

Here is what I came up with to perform a Django testing stage using a project setup similar to Michael’s:

jobs:
  run_tests:
    if: github.ref == 'refs/heads/develop'
    name: Run Django Tests
    runs-on: ubuntu-latest
    services:
      db:
        image: postgres:12.3-alpine
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: github_actions
        ports:
          - 5432:5432
        options: --mount type=tmpfs,destination=/var/lib/postgresql/data --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
    steps:
      - name: Checkout
        uses: actions/checkout@v1
        with:
          ref: 'refs/heads/develop'
      - name: Set up Python 3.8.5
        uses: actions/setup-python@v1
        with:
          python-version: 3.8.5
      - name: Install dependencies
        run: pip install -r app/requirements.txt
      - name: Run tests
        run: python app/manage.py test app/
        env:
          SYSTEM_ENV: GITHUB_WORKFLOW

Explanation:

Line 2: This job sits at the same indentation level as the build and deploy jobs used in Michaels’ example Github Actions workflow file.

Line 3: Higher up in your workflow, your action should be on: [push]. Line 3 tells Github Actions to only run the testing if the push was to a branch called develop. You can change this to suit your project, though note it will also need to be updated below.

Line 8: Note that the postgres service is from Alpine. Most services use the general postgres container, but that’s not necessary. Alpine is smallest docker container version of postgres I’m aware of, and you should choose it to help make this job as fast as possible.

Lines 9-12: These environment variables are used by the postgres image in setting up your database that will be used for test. These must match what your project’s settings.py file uses when it is run by Github Actions

Line 13: It is simplest to explicitly declare 5432 as the port for this job.

Line 15: These options ensure the database is ready before allowing the job to continue to run the tests. In addition, a temporary filesystem (tmpfs) is mounted. h/t @AdamChainz

Line 20: This specifies where the code that contains the tests you’re running will be. This line should match Line 3 above: you want to test the branch you’ve just pushed to.

Lines 21 and 25: Presuming your requirements.txt file contains the expected postgres dependencies, these are the only two steps you need to perform in order to get to running your django tests. Some guides suggest you need to run migrations or install postgres dependencies manually. I did not find this to be the case.

Line 28: Note that you must specify the project path twice in the test command. Normally, in pycharm for example, you don’t notice this is needed but try running ./manage.py test from the parent directory to /app without the second app/ and you’ll see zero tests are run.

Line 30: This line is crucial! It is passed means that the SYSTEM_ENV environment variable is set to GITHUB_WORKFLOW at the time your django project is run by Github Actions.

This is important because you will need to customize the DATABASE portion of your settings.py so that it will be set appropriately for this stage of Github Actions.

Configuring your Django Settings for Github Actions Django Testing Job

SYSTEM_ENV = os.environ.get('SYSTEM_ENV', None)
if SYSTEM_ENV == 'PRODUCTION' or SYSTEM_ENV == 'STAGING':
    DEBUG = False
    SECRET_KEY = os.environ.get('SECRET_KEY', None)
    ...
    CSRF_COOKIE_SECURE = True
    ...
    # Set Sentry and Error Debug
    import sentry_sdk
    ...
elif SYSTEM_ENV == 'GITHUB_WORKFLOW':
    DEBUG = True
    SECRET_KEY = 'TESTING_KEY'
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.postgresql',
            'NAME': 'github_actions',
            'USER': 'postgres',
            'PASSWORD': 'postgres',
            'HOST': '127.0.0.1',
            'PORT': '5432',
        }
    }
elif SYSTEM_ENV == 'DEVELOPMENT':
    DEBUG = True

The above shows the settings override section of a django project that allows you to detect for the SYSTEM_ENV set in an environment variable.

For the example in this blog entry, you only need to pay attention to Lines 11-23. However, I included the declaration of the SYSTEM_ENV (Line 1) and the conditional detection of production and staging environments to help show how SYSTEM_ENV can be used to effectively switch Django settings overrides for dynamic CI like Github Actions can allow.

Pay attention to lines 17-21. These need to match the settings used for the db service in the job’s postgres service declaration above. If you don’t get the match right, or something is funky with your settings overrides, you may get an error like:

django.db.utils.OperationalError: could not translate host name "db" to address: Temporary failure in name resolution

I give an extended explanation of how to resolve this in this Stack Overflow answer.

Notes on Debugging Github Actions

As I mention in the above SO answer, one of the problems with Github Actions is the delay between trying something and waiting for feedback from Github.

The disconnect a change and the result can disguise things you might otherwise catch in local development. For example, you may think you’re working on relevant code / pushing to the right place / setting the correct environment variables when you are not.

Part of the reason this might happen is because you started browsing the web or were otherwise distracted waiting to see if your fix worked. I am guilty of this.

Another pain is that in writing Github Actions that act on a push, you may find yourself pushing to a branch that you don’t want to bunch of clumsy changes in.

If your commit message quality adherence is strong, it can become a bit annoying to have to keep explaining changes and reverts of those changes as you test things out.

Improvements to this example

This is just one job out of what can be an awesome CI/CD workflow for Django with Github Actions. For example, you would probably not want to deploy a copy of your Django project that fails the testing job!

With a complete setup you could add a

needs: [build_develop, run_tests]

to your deploy_staging job, which would allow your build and test jobs to run in parallel! I won’t get into that here, but it really is magic when it is all working.

Specific to this guide, I have no doubt that this can be improved upon as I’m still learning the ways of Github Actions myself. Please feel free to suggest them in the comments, hit me on twitter or email me.

Additional Links on This Topic

Here are some additional links and resources I found while learning about this topic: