Automating Django Tests with GitHub Actions (CI/CD)
Deployment should not be scary. If you are afraid to push code on a Friday, it means you lack confidence in your testing pipeline.
Continuous Integration (CI) is the practice of automating the integration of code changes. In this tutorial, we will set up GitHub Actions to run our Django tests and lint our code automatically.
What is GitHub Actions?
GitHub Actions is a CI/CD platform built directly into GitHub. It allows you to define "Workflows" (YAML files) that trigger on specific events, like a push or pull_request.
Step 1: The Workflow File
Create a directory in your project root: .github/workflows/. Inside, create a file named django_test.yml.
name: Django CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
# Service Containers: This spins up a temporary Postgres DB for testing
services:
postgres:
image: postgres:13
env:
POSTGRES_DB: test_db
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install flake8
- name: Lint with Flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Run Tests
env:
# These match the service container above
DB_ENGINE: django.db.backends.postgresql
DB_NAME: test_db
DB_USER: user
DB_PASSWORD: password
DB_HOST: localhost
DB_PORT: 5432
run: |
python manage.py test
Step 2: Preparing Your Django Settings
Your settings.py needs to be able to read these environment variables. In a production or CI environment, you shouldn't hardcode database credentials.
import os
DATABASES = {
'default': {
'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.sqlite3'),
'NAME': os.environ.get('DB_NAME', 'db.sqlite3'),
'USER': os.environ.get('DB_USER', ''),
'PASSWORD': os.environ.get('DB_PASSWORD', ''),
'HOST': os.environ.get('DB_HOST', ''),
'PORT': os.environ.get('DB_PORT', ''),
}
}
Step 3: Linting
Notice the Lint with Flake8 step in the YAML file? Linting checks your code for stylistic errors (like unused imports or indentation issues) before running the heavy tests. It's a "fail fast" mechanism.
Create a .flake8 configuration file in your root to ignore specific migrations files (which are auto-generated and often fail strict linting):
[flake8]
exclude = .git,__pycache__,migrations,env
max-line-length = 120
The Workflow
- You push code to GitHub.
- GitHub notices the
.github/workflowsfile. - It allocates a Linux server.
- It spins up a Docker container for Postgres.
- It installs Python and your dependencies.
- It runs
python manage.py test.
If any test fails, you get a big red X on your Pull Request, preventing you from merging broken code.
Conclusion
Setting up CI takes about 15 minutes, but it saves hours of debugging "production bugs" later. It creates a safety net that allows you to refactor code with confidence.