- Published on
Setup CI for django unit testing with github actions
- Authors
- Name
- Karan Kumar
- @scripter_x
Github actions allows to automate all your software workflows and build customizable CI/CD pipeline.
This blog assumes you have some knowledge of Django
but are new to github actions workflows.
Contents:
Setup django's database settings to be read from environment
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('MYSQL_DATABASE'),
'USER': os.environ.get('MYSQL_USER'),
'PASSWORD': os.environ.get('MYSQL_PASSWORD'),
'HOST': os.environ.get('MYSQL_HOST'),
'PORT': os.environ.get('MYSQL_PORT'),
}
}
Setup github workflow
1. Download the template from github marketplace
We are using a workflow template provided by github and modifying it to meet our needs.
Note: Github actions files are stored in your .github/workflows/<file-name>.yml
and are to be committed.
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Python application
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- 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
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
Let first try to understand what is happening in the given template
name: Python application # name of the work flow
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#on
on:
push: # whenever there is push event
branches: [ master ] # on master branch
pull_request: # whenever there is pull request created
branches: [ master ] # with target branch as master
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobs
jobs: # jobs is reserved namespace
build: # a unique identifier for your job. This can be anything and translates to jobs.<job_id>
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
runs-on: ubuntu-latest # The type of machine to run the job on
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idsteps
steps: # sequence of tasks called steps
You can define sequential steps to be run in the steps
key
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsuses
- uses: actions/checkout@v2 # official github provided action. Checks out code for processing
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepswith
- name: Set up Python 3.9 # name for easy identification
uses: actions/setup-python@v2 # official github provided action. Installs Python on your runs-on machine
with: # with provides an ability to pass arguments to action
python-version: 3.9 # python-version to be installed
Other steps
in template are intuitive to understand.
mysql
service
2. Edit the template to setup Define the services
key under jobs.<job_id>
as following
...
jobs:
build:
runs-on: ubuntu-latest
env:
MYSQL_PASSWORD: password
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: ${{ env.MYSQL_PASSWORD }}
ports:
- 3306:3306
...
If you have worked with docker the syntax would be very intuitive to understand. We are basically installing mysql:8.0
image and passing some required variable MYSQL_ROOT_PASSWORD
for setup. The important parts here are the ports
& options
.
Port 3306
is exposed from the container service and it mapped to 3306
port on the host machine (github machine).
3. Setup health check for our mysql service
Containers takes sometime to get mysql
ready for accepting connections. Therefore it is important that we wait for mysql
service to become healthy before proceeding with steps
...
jobs:
build:
runs-on: ubuntu-latest
env:
MYSQL_PASSWORD: password
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: ${{ env.MYSQL_PASSWORD }}
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
...
- We are passing
health-cmd
inoptions
to change the container healthy check command. - If
mysqladmin ping
does not respond withinhealth-timeout
we waithealth-interval
seconds to try again unless we have triedhealth-retries
times. Ifmysqladmin ping
responds without error. We establish thatmysql
inside the container is working perfectly and is ready to accept connections.
Lets just add more environment variables for mysql
service creation, this will allow us to have same mysql
user, port, host & database for django's
database settings
...
runs-on: ubuntu-latest
env:
MYSQL_PASSWORD: password
MYSQL_HOST: 127.0.0.1
MYSQL_PORT: 3306
MYSQL_DATABASE: test_database
MYSQL_USER: mysql_user
ENVIRONMENT: CI
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: ${{ env.MYSQL_PASSWORD }}
MYSQL_PASSWORD: ${{ env.MYSQL_PASSWORD }}
MYSQL_HOST: ${{ env.MYSQL_HOST }}
MYSQL_PORT: ${{ env.MYSQL_PORT }}
MYSQL_DATABASE: ${{ env.MYSQL_DATABASE }}
MYSQL_USER: ${{ env.MYSQL_USER }}
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
...
4. Setup permissions for mysql user
To avoid any permission errors like:
Got an error creating the test database: permission denied to create database
django.db.utils.OperationalError: (1045, “Access denied for user 'root'@'localhost'
The magic command to fix those error in our test environment is
GRANT ALL PRIVILEGES ON *.* TO '${{ env.MYSQL_USER }}'@'%'
Our complete file at .github/workflows/python-app.yml
looks like this.
Note the highlighted area in the below code.We want to allow our mysql user to have all rights to database.
# .github/workflows/python-app.yml
name: Python application
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
env:
MYSQL_PASSWORD: password
MYSQL_HOST: 127.0.0.1
MYSQL_PORT: 3306
MYSQL_DATABASE: test_database
MYSQL_USER: mysql_user
ENVIRONMENT: CI
services:
mysql:
image: mysql:8.0
env:
MYSQL_HOST: ${{ env.MYSQL_HOST }}
MYSQL_PORT: ${{ env.MYSQL_PORT }}
MYSQL_DATABASE: ${{ env.MYSQL_DATABASE }}
MYSQL_USER: ${{ env.MYSQL_USER }}
MYSQL_PASSWORD: ${{ env.MYSQL_PASSWORD }}
MYSQL_ROOT_PASSWORD: ${{ env.MYSQL_PASSWORD }}
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Grant permission to mySQL and Flush privileges
run: |
mysql --host ${{ env.MYSQL_HOST }} --port ${{ env.MYSQL_PORT }} -uroot -p${{ env.MYSQL_PASSWORD }} -e "GRANT ALL PRIVILEGES ON *.* TO '${{ env.MYSQL_USER }}'@'%';FLUSH PRIVILEGES;"
- uses: actions/checkout@v2
- name: Set up Python 3.6
uses: actions/setup-python@v2
with:
python-version: 3.6
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Run migrations
run: python manage.py migrate
- name: Test with django
run: |
python manage.py test
5. Commit and create PR
Open a PR to master with file .github/workflows/python-app.yml
committed and you will see github action workflow picks it up and execute all the steps.