Django Graphene
Hopefully, after chapter 2 you’re getting the hang of test driven development aka TDD. I find it most empowering run tests to see OK. Next up, is setting Django Graphene.
Installation of Django Graphene
Way back in part 1 of this tutorial you created a requirements.txt file which should include graphene-django==2.2.0 alone with all the other goodies.
If you check if you do have graphene-django installed you can run the following command:
docker-compose run server pip3 freeze
This command will print a list of all your installed packages.
If you don’t find graphene-django in your requirements.txt you can update /server/requirements.txt with the following:
Django>=3.0
psycopg2>=2.8.6
graphene-django>=2.2.0
django-autoslug>=1.9.6
django-filter>=2.0.0
django-graphql-jwt>=0.1.5
django-mptt>=0.11.0
Pillow>=6.1.0
django-cors-headers>=3.1.0
django-jwt-auth>=0.0.2
PyJWT>=1.7.1
coverage>=5.1
freezegun>=0.3.15
python-dateutil>=2.8.1
pytest-django>=3.9.0
If you did have to add graphene-django to the requirements.txt we need to rebuild our server again to install graphene-django along with all its dependencies:
docker-compose build server
Configuring Django Graphene
In the todo-app-graphql/server/todo_proj/settings.py file find INSTALLED_APPS and add the following:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'todo_app',
'mptt',
'graphene_django',
]
At the very bottom of the settings.py file add:
# todo-app-graphql/server/todo_proj/settings.py
# ... code
GRAPHENE = {
'SCHEMA': 'todo_proj.schema.schema',
}
Now your todo-app-graphql/server/todo_proj/settings.py should look like the following. Note the highlighted changes we just made.
# todo-app-graphql/server/todo_proj/settings.py
"""
Django settings for todo_proj project.
Generated by 'django-admin startproject' using Django 3.2.5.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-z6a#15u#4bw092uhpnvhbc_h!p$!&&g&qvb5dq3vj7kj3@#%bg'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'todo_app',
'mptt',
'graphene_django',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'todo_proj.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'todo_proj.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
import os
DATABASES = {
'default':
{
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DATABASE_NAME'),
'USER': os.environ.get('DATABASE_USER'),
'PASSWORD': os.environ.get('DATABASE_PASSWORD'),
'HOST': os.environ.get('DATABASE_HOST'),
'PORT': os.environ.get('DATABASE_PORT'),
}
}
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = '/static/'
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
GRAPHENE = {
'SCHEMA': 'todo_proj.schema.schema',
}
We will need to reboot the server for these changes to take effect:
docker-compose down
docker-compose up -d
# if you run into problems with the above you can start the services separately insuring that the database is up and running before we boot up the server
docker-compose down
docker-compose up -d database
docker-compose up -d server
The Wonders of GraphiQL
GraphiQL is our graphical interactive playground in-browser GraphQL IDE. How cool is that.
Note that we will need to disable Django CSRF protection.
Install GraphiQL by adding the highlighted lines below to todo_proj/urls.py file:
# todo-app-graphql/server/todo_proj/urls.py
from django.contrib import admin
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView
urlpatterns = [
path('admin/', admin.site.urls),
path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]
Now we should be able to access GraphiQL at http://localhost:5555/graphql/
Add the following to GraphiQL and we should a list of all the todos we have entered from the Django admin UI or the Django shell.
query {
todos {
id
title
task
}
}
We should then see…

Writing Tests for Django-Graphene
In our last post we wrote tests for the model confirming, (or not), that we were able to read and add data from the postgres db. For the next step lets write tests for listing our data from graphql.
Structure of a Graphene Test
We are going to be doing the following:
- Import GraphQLTestCase
- Import our models
- import json to process in python, (more on Django + JSON)
- Create a class where we pass in the GraphQLTestCase class
- Create a function to setup our data
- Create a function to test listing all todos
- Assert that there are no errors in our response
Create a new server/todo_app/tests/test_graphql_queries.py file:
touch server/todo_app/tests/test_graphql_queries.py
… and add the following tests which test for returning all todos, projects and categories as well as getting specific todo, project and category by id:
# server/todo_app/tests/test_graphql_queries.py
from graphene_django.utils.testing import GraphQLTestCase
from todo_app.models import Todo, Project, Category
import json
class QueryTestCases(GraphQLTestCase):
def test_all_todos(self):
response = self.query(
'''
query GetTodos {
todos {
id
title
task
slug
project {
id
name
slug
}
category {
id
name
slug
}
}
}
'''
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_todo_by_id_query(self):
proj1 = Project.objects.create(name='proj1')
cat1 = Category.objects.create(name='cat1')
todo1 = Todo.objects.create(id=1, title='todo1', task='todo1 task', project=proj1, category=cat1)
response = self.query(
'''
query Todo($id: Int!) {
todo(id:$id) {
id
title
task
slug
project {
name
slug
}
category {
name
slug
}
}
}
''',
variables = {
'id': 1
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_all_projects(self):
proj1 = Project.objects.create(name='proj1')
cat1 = Category.objects.create(name='cat1')
todo1 = Todo.objects.create(title='todo1', task='todo1 task', project=proj1, category=cat1)
response = self.query(
'''
query GetProjects {
projects {
id
name
slug
todoProject {
id
title
task
slug
category {
id
name
slug
}
}
}
}
'''
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_project_by_id_query(self):
proj1 = Project.objects.create(id=1, name='proj1')
cat1 = Category.objects.create(name='cat1')
todo1 = Todo.objects.create(title='todo1', task='todo1 task', project=proj1, category=cat1)
response = self.query(
'''
query Project($id: Int!) {
project(id:$id) {
id
name
slug
todoProject {
id
title
task
slug
category {
id
name
slug
}
}
}
}
''',
variables = {
'id': 1
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_all_categories_query(self):
response = self.query(
'''
query GetCategories {
categories {
id
name
todos {
id
title
project {
id
name
}
}
}
}
'''
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_get_category_by_id(self):
cat1 = Category.objects.create(id=1, name='cat1')
proj1 = Project.objects.create(id=1, name='proj1')
todo = Todo.objects.create(id=1, title='todo', task='todo task', project=proj1)
response = self.query(
'''
query GetCategory($id: Int!) {
category(id: $id) {
id
name
slug
todos {
id
title
task
slug
project {
id
name
slug
}
}
}
}
''',
variables = {
'id': 1
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
If we were to run our tests now they would fail since we don’t have a schema for Graphene, so now we create our schemas.
Creating Our First Type & Schema
A Type is an object that may contain multiple fields. A collection of types is called a schema.
Every schema has a type called query for pulling data from our server and another type called mutation for sending data to our server.
This is a way over-simplified explanation of GraphQL concepts but it’s enough to keep plowing on through with. Want to dig into the nitty gritty go here.
Now we create our todo_app/schema.py file:
touch server/todo_app/schema.py
… and populate the schema.py with the following:
# todo-app-graphql/server/todo_app/schema.py
import graphene
from graphene import Schema
from graphene_django import DjangoObjectType
from graphql import GraphQLError
from todo_app.models import Todo, Project, Category
class TodoType(DjangoObjectType):
class Meta:
model = Todo
class ProjectType(DjangoObjectType):
class Meta:
model = Project
class CategoryType(DjangoObjectType):
class Meta:
model = Category
class Query(graphene.ObjectType):
# todo queries
todos = graphene.List(TodoType)
def resolve_todos(self, info, **kwargs):
return Todo.objects.all()
todo = graphene.Field(
TodoType,
id=graphene.Int(),
)
def resolve_todo(self, info, id):
return Todo.objects.get(pk=id)
# project queries
projects = graphene.List(ProjectType)
def resolve_projects(self, info, **kwargs):
return Project.objects.all()
project = graphene.Field(
ProjectType,
id=graphene.Int(),
)
def resolve_project(self, info, id):
return Project.objects.get(pk=id)
# category queries
categories = graphene.List(CategoryType)
def resolve_categories(self, info, **kwargs):
return Category.objects.all()
category = graphene.Field(
CategoryType,
id=graphene.Int(),
)
def resolve_category(self, info, id):
return Category.objects.get(pk=id)
schema = Schema(query=Query)
Above we created a TodoType using the DjangoObjectType which is a custom type from Graphene Django.
We also created a special type Query with a resolver for the field tasks, which returns all our Todo tasks.
Then we extended the above to definitions for the ProjectType and the CategoryType.
Now we create our todo_proj/schema.py file
touch server/todo_proj/schema.py
… and populate with the following Query type:
# todo-app-graphql/server/todo_proj/schema.py
import graphene
import todo_app.schema
class Query(
todo_app.schema.Query,
graphene.ObjectType
):
pass
schema = graphene.Schema(query=Query)
The above snippet inherits the query we defined in our todo_app. This allows us to keep all our app schemas isolated in their respective apps.
Running Our First PyTest
In Part 1 among the many server/requirements.txt Django packages we installed was pytest-django>=3.9.0. I prefer PyTest to Django test because its cleaner, gives more feedback on the status of the tests whether they succeed or fail.
With our following files in place:
- /server/todo_app/tests/test_graphql.py
- /server/todo_app/schema.py
- /server/todo_proj/schema.py
… we can now run all our tests including the graphene query test using the pytest command:
docker-compose run server pytest
[+] Running 1/0
⠿ Container postgres Running 0.0s
============================================= test session starts ==============================================
platform linux -- Python 3.9.6, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
django: settings: todo_proj.settings (from env)
rootdir: /server
plugins: django-4.4.0
collected 11 items
todo_app/tests/test_graphql_queries.py ...... [ 54%]
todo_app/tests/test_models.py ..... [100%]
=============================================== warnings summary ===============================================
../usr/local/lib/python3.9/site-packages/django/apps/registry.py:91
/usr/local/lib/python3.9/site-packages/django/apps/registry.py:91: RemovedInDjango41Warning: 'mptt' defines default_app_config = 'mptt.apps.MpttConfig'. Django now detects this configuration automatically. You can remove default_app_config.
app_config = AppConfig.create(entry)
-- Docs: https://docs.pytest.org/en/stable/warnings.html
======================================== 11 passed, 1 warning in 4.14s =========================================
Success! If we had run into some failures we can use the pytest verbose commands:
- docker-compose run server pytest -v # which displays failures in detail
- docker-compose run server pytest -vv # which displays failures in even more detail
Next post we introduce mutaions.