Mutations
In order to post data to a server with Graphql we use a process called mutations which are similar to the query process we defined in our last post.
TDD Graphene Mutations
We’ll begin by writing a test for creating a new todo item.
Create a new file server/todo_app/tests/test_graphql_mutations.py:
touch server/todo_app/tests/test_graphql_mutations.py
Populate test_graphql_mutations.py with the following:
# server/todo_app/tests/test_graphql_mutations.py
from graphene_django.utils.testing import GraphQLTestCase
from todo_app.models import Todo, Project, Category
import json
class MutationTestCases(GraphQLTestCase):
def test_create_todo(self):
proj1 = Project(name='proj1')
proj1.save()
cat1 = Category(name='cat1')
cat1.save()
response = self.query(
'''
mutation createTodo($title: String!, $task: String!, $projId: Int!, $catId: Int!) {
createTodo(title: $title, task: $task, projId: $projId, catId: $catId) {
title
task
slug
project {
name
slug
}
category {
name
slug
}
}
}
''',
variables = {
'title': 'todo',
'task': 'task',
'projId': proj1.id,
'catId': cat1.id,
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
Todo App Mutations Schema
Now we know we have to create a schema for this create todo mutation:
# server/todo_app/schema.py
# code ...
#1
# Mutations
class CreateTodo(graphene.Mutation):
id = graphene.Int()
title = graphene.String()
task = graphene.String()
is_completed = graphene.Boolean()
slug = graphene.String()
project = graphene.Field(ProjectType)
category = graphene.Field(CategoryType)
#2
class Arguments:
title = graphene.String()
task = graphene.String()
projId = graphene.Int()
catId = graphene.Int()
#3
def mutate(self, info, title, task, projId, catId):
proj = Project.objects.get(pk=projId)
cat = Category.objects.get(pk=catId)
todo = Todo.objects.create(
title=title,
task=task,
project=proj,
category=cat,
)
return CreateTodo(
id=todo.id,
title=todo.title,
task=todo.task,
slug=todo.slug,
project=todo.project,
category=todo.category,
)
#4
class Mutation(graphene.ObjectType):
create_todo = CreateTodo.Field()
schema = Schema(query=Query, mutation=Mutation)
What is going on here?
- #1: Define a mutation class, define the resulting output of the mutation which is the data the server sends back to the client.
- #2: Define the arguments that the server is expecting.
- #3: Define the mutation method that creates a new task in the database with the data the user sent. After the server returns the CreateTask class with our new data which matches the parameters in #1.
- #4: Create a mutation class with the field to be resolved.
Your updated server/todo_app/schema.py should be as follows:
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)
# Mutations
class CreateTodo(graphene.Mutation):
id = graphene.Int()
title = graphene.String()
task = graphene.String()
is_completed = graphene.Boolean()
slug = graphene.String()
project = graphene.Field(ProjectType)
category = graphene.Field(CategoryType)
class Arguments:
title = graphene.String()
task = graphene.String()
projId = graphene.Int()
catId = graphene.Int()
def mutate(self, info, title, task, projId, catId):
proj = Project.objects.get(pk=projId)
cat = Category.objects.get(pk=catId)
todo = Todo.objects.create(
title=title,
task=task,
project=proj,
category=cat,
)
return CreateTodo(
id=todo.id,
title=todo.title,
task=todo.task,
slug=todo.slug,
project=todo.project,
category=todo.category,
)
class Mutation(graphene.ObjectType):
create_todo = CreateTodo.Field()
schema = Schema(query=Query, mutation=Mutation)
… update server/todo_proj/schema.py with our new mutation:
# server/todo_proj/schema.py
import graphene
import todo_app.schema
class Query(
todo_app.schema.Query,
graphene.ObjectType
):
pass
class Mutation(
todo_app.schema.Mutation,
graphene.ObjectType
):
pass
# update the schema variable with our mutation
schema = graphene.Schema(query=Query, mutation=Mutation)
Now we can run our tests with pytest.
docker-compose run server pytest
If all goes well we should see a listing of passed tests including our new create todo mutation test.
=============================================== test session starts ===============================================
platform linux -- Python 3.8.5, pytest-6.1.1, py-1.9.0, pluggy-0.13.1 -- /usr/local/bin/python
cachedir: .pytest_cache
django: settings: todo_proj.settings (from env)
rootdir: /var/www/server
plugins: django-4.0.0
collected 7 items
todo_app/tests/test_graphql_mutations.py::MutationTestCases::test_create_todo PASSED
todo_app/tests/test_graphql_queries.py::QueryTestCases::test_all_todos PASSED
todo_app/tests/test_models.py::CategoryModelTest::test_string_representation PASSED
todo_app/tests/test_models.py::TodoModelTest::test_string_representation PASSED
todo_app/tests/test_models.py::TodoModelTest::test_todo_field PASSED
todo_app/tests/test_models.py::ProjectModelTest::test_project_field PASSED
todo_app/tests/test_models.py::ProjectModelTest::test_string_representation PASSED
If your test fails you can always run pytest in verbose mode with the -vv flag:
docker-compose run server pytest -vv
In verbose mode pytest may tell you:
/usr/local/lib/python3.8/site-packages/graphene_django/utils/testing.py:112: in assertResponseNoErrors
self.assertEqual(resp.status_code, 200)
E AssertionError: 400 != 200
… which is not much, other than your mutation has an error somewhere in the code. In which case you can add another level of granularity to our mutation test using assert content == to an expected result.
# server/todo_app/tests/test_graphql_mutations.py
from graphene_django.utils.testing import GraphQLTestCase
from todo_app.models import Todo, Project, Category
import json
class MutationTestCases(GraphQLTestCase):
def test_create_todo(self):
proj1 = Project(name='proj1')
proj1.save()
cat1 = Category(name='cat1')
cat1.save()
response = self.query(
'''
mutation createTodo($title: String!, $task: String!, $projId: Int!, $catId: Int!) {
createTodo(title: $title, task: $task, projId: $projId, catId: $catId) {
title
task
slug
project {
name
slug
}
category {
name
slug
}
}
}
''',
variables = {
'title': 'todo',
'task': 'task',
'projId': proj1.id,
'catId': cat1.id,
}
)
expected = {
'data': {
'createTodo': {
'title': 'todo',
'task': 'task',
'slug': 'todo',
'project': {
'name': 'proj1',
'slug': 'proj1',
},
'category': {
'name': 'cat1',
'slug': 'cat1',
}
}
}
}
content = json.loads(response.content)
assert content == expected
self.assertResponseNoErrors(response)
I will go this extra mile if I do get the AssertionError: 400 != 200 error in which case the assert content == expected proves most helpful in tracking down the problem in the verbose test result.
Complete Schema & Tests for All Crud Operations
We have the create operation for creating a todo but, now we need update and delete operations for both the schema and mutation tests which following in the same vein as our create todo mutation.
We also need to extend our schema and tests for all the crud operations for the Project and Category models.
Again we’ll start with tests. Below are tests for the crud operations of all our models:
# server/todo_app/tests/test_graphql_mutations.py
from graphene_django.utils.testing import GraphQLTestCase
from todo_app.models import Todo, Project, Category
import json
class MutationTestCases(GraphQLTestCase):
def test_create_todo(self):
proj1 = Project(name='proj1')
proj1.save()
cat1 = Category(name='cat1')
cat1.save()
response = self.query(
'''
mutation createTodo($title: String!, $task: String!, $projId: Int!, $catId: Int!) {
createTodo(title: $title, task: $task, projId: $projId, catId: $catId) {
title
task
slug
project {
name
slug
}
category {
name
slug
}
}
}
''',
variables = {
'title': 'todo',
'task': 'task',
'projId': proj1.id,
'catId': cat1.id,
}
)
expected = {
'data': {
'createTodo': {
'title': 'todo',
'task': 'task',
'slug': 'todo',
'project': {
'name': 'proj1',
'slug': 'proj1',
},
'category': {
'name': 'cat1',
'slug': 'cat1',
}
}
}
}
content = json.loads(response.content)
assert content == expected
self.assertResponseNoErrors(response)
def test_update_todo_name(self):
# Test for updating a todos name
proj1 = Project(name='proj1')
proj1.save()
cat1 = Category(name='cat1')
cat1.save()
todo = Todo(title='todo', task='todo task', project=proj1, category=cat1)
todo.save()
response = self.query(
'''
mutation updateTodo($id: Int!, $title: String!) {
updateTodo(id: $id, title: $title){
id
slug
title
task
project {
slug
name
}
category {
slug
name
parent {
slug
name
}
}
}
}
''',
variables = {
'id': todo.id,
'title': 'todo updated',
}
)
content = json.loads(response.content)
#assert content == {}
self.assertResponseNoErrors(response)
def test_update_todo_task(self):
# Test for updating a todos task
proj1 = Project(name='proj1')
proj1.save()
cat1 = Category(name='cat1')
cat1.save()
todo = Todo(title='todo', task='todo task', project=proj1, category=cat1)
todo.save()
response = self.query(
'''
mutation updateTodo($id: Int!, $task: String!) {
updateTodo(id: $id, task: $task){
id
slug
title
task
project {
slug
name
}
category {
slug
name
parent {
slug
name
}
}
}
}
''',
variables = {
'id': todo.id,
'task': 'todo updated task',
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_update_todo_project(self):
# Test for updating a todos project
proj1 = Project(name='proj1')
proj1.save()
cat1 = Category(name='cat1')
cat1.save()
todo = Todo(title='todo', task='todo task', project=proj1, category=cat1)
todo.save()
proj2 = Project(name='proj2')
proj2.save()
response = self.query(
'''
mutation updateTodo($id: Int!, $projId: Int!) {
updateTodo(id: $id, projId: $projId){
id
slug
title
task
project {
slug
name
}
category {
slug
name
parent {
slug
name
}
}
}
}
''',
variables = {
'id': todo.id,
'projId': proj2.id,
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_update_todo_category(self):
# Test for updating a todos category
proj1 = Project(name='proj1')
proj1.save()
cat1 = Category(name='cat1')
cat1.save()
todo = Todo(title='todo', task='todo task', project=proj1, category=cat1)
todo.save()
cat2 = Category(name='cat2')
cat2.save()
response = self.query(
'''
mutation updateTodo($id: Int!, $catId: Int!) {
updateTodo(id: $id, catId: $catId){
id
slug
title
task
project {
slug
name
}
category {
slug
name
parent {
slug
name
}
}
}
}
''',
variables = {
'id': todo.id,
'catId': cat2.id,
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_update_todo_is_completed(self):
# Test for updating a todos is_completed status
proj1 = Project(name='proj1')
proj1.save()
cat1 = Category(name='cat1')
cat1.save()
todo = Todo(title='todo', task='todo task', project=proj1, category=cat1)
todo.save()
response = self.query(
'''
mutation updateTodo($id: Int!, $isCompleted: Boolean!) {
updateTodo(id: $id, isCompleted: $isCompleted){
id
slug
title
task
isCompleted
project {
slug
name
}
category {
slug
name
parent {
slug
name
}
}
}
}
''',
variables = {
'id': todo.id,
'isCompleted': True,
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_update_todo_everything(self):
# Test for updating all of a todo's attributes at once
proj1 = Project(name='proj1')
proj1.save()
cat1 = Category(name='cat1')
cat1.save()
proj2 = Project(name='proj2')
proj2.save()
cat2 = Category(name='cat2')
cat2.save()
todo = Todo(title='todo', task='todo task', project=proj1, category=cat1)
todo.save()
response = self.query(
'''
mutation updateTodo(
$id: Int!,
$title: String!,
$task: String!,
$projId: Int!,
$catId: Int!,
$isCompleted: Boolean!
) {
updateTodo(
id: $id,
title: $title,
task: $task,
projId: $projId,
catId: $catId,
isCompleted: $isCompleted,
){
id
slug
title
task
isCompleted
project {
slug
name
}
category {
slug
name
parent {
slug
name
}
}
}
}
''',
variables = {
'id': todo.id,
'title': 'updated title',
'task': 'updated task',
'projId': proj2.id,
'catId': cat2.id,
'isCompleted': True,
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_delete_todo(self):
todo = Todo.objects.create(title='todo')
response = self.query(
'''
mutation DeleteTodo($id: Int!) {
deleteTodo(id: $id) {
id
title
}
}
''',
variables = {
'id': todo.id
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_create_project(self):
response = self.query(
'''
mutation createProject($name: String!, $description: String!) {
createProject(name: $name, description: $description) {
id
name
description
slug
}
}
''',
variables = {
'name': 'My Project',
'description': 'Description for my project',
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_delete_project(self):
project = Project.objects.create(name='foo')
response = self.query(
'''
mutation deleteProject($id: Int!) {
deleteProject(id: $id) {
id
name
}
}
''',
variables = {
'id': project.id,
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_update_project_name(self):
project = Project.objects.create(name='foo')
# Test updating both name of a project
response = self.query(
'''
mutation updateProject($id: Int!, $name: String!) {
updateProject(id:$id, name: $name) {
id
name
description
slug
}
}
''',
variables = {
'id': project.id,
'name': 'My Project',
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_update_project_name_description(self):
project = Project.objects.create(name='foo')
# Test updating both name and description of a project
response = self.query(
'''
mutation updateProject($id: Int!, $name: String!, $description: String!) {
updateProject(id:$id, name: $name, description: $description) {
id
name
description
slug
}
}
''',
variables = {
'id': project.id,
'name': 'My Project',
'description': 'Description for my project',
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_update_project_description(self):
# Test updating of project description
project = Project.objects.create(name='foo', description='bar')
response = self.query(
'''
mutation updateProject($id: Int!, $description: String!) {
updateProject(id:$id, description: $description) {
id
name
description
slug
}
}
''',
variables = {
'id': project.id,
'description': 'Description for my project',
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_create_category_no_parent(self):
response = self.query(
'''
mutation createCategory($name: String!) {
createCategory(name: $name) {
id
name
slug
}
}
''',
variables = {
'name': 'my category',
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_create_category_with_parent(self):
parent = Category.objects.create(name='Category Parent')
response = self.query(
'''
mutation createCategory($name: String!, $parentId: Int!) {
createCategory(name: $name, parentId: $parentId) {
id
name
slug
}
}
''',
variables = {
'name': 'my category',
'parentId': parent.id
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_delete_category(self):
category = Category.objects.create(name='foo')
response = self.query(
'''
mutation deleteCategory($id: Int!) {
deleteCategory(id: $id) {
id
name
}
}
''',
variables = {
'id': category.id,
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_update_category_name(self):
category = Category.objects.create(name='foo')
# Test for changing a category's name
response = self.query(
'''
mutation updateCategory($id: Int!,$name: String!){
updateCategory(id: $id, name: $name) {
id
name
slug
}
}
''',
variables = {
'id': category.id,
'name': 'New Category Name',
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_update_category_parent(self):
categoryParent = Category.objects.create(name='Parent Category')
category = Category.objects.create(name='foo')
# Test for moving category into a parent category.
response = self.query(
'''
mutation updateCategory($id: Int!,$parentId: Int!){
updateCategory(id: $id, parentId: $parentId) {
id
name
slug
parent {
id
name
slug
}
}
}
''',
variables = {
'id': category.id,
'parentId': categoryParent.id,
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
def test_update_category_name_parent(self):
categoryParent = Category.objects.create(name='category parent')
category = Category.objects.create(name='foobar')
# Test for changing a category's name & moving renamed category into a parent category
response = self.query(
'''
mutation updateCategory($id: Int!, $name: String!, $parentId: Int!){
updateCategory(id: $id, name: $name, parentId: $parentId) {
id
name
slug
parent {
id
name
slug
}
}
}
''',
variables = {
'id': category.id,
'name': 'New Category Name',
'parentId': categoryParent.id
}
)
content = json.loads(response.content)
self.assertResponseNoErrors(response)
Running these tests will throw multiple errors which clarifies our thinking in the sense that pytest is telling us what object types, fields and resolvers we need to add to our schema.
The following is a complete update of the todo app schema which should enable us to pass all our tests:
# 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)
# Mutations
class CreateTodo(graphene.Mutation):
id = graphene.Int()
title = graphene.String()
task = graphene.String()
is_completed = graphene.Boolean()
slug = graphene.String()
project = graphene.Field(ProjectType)
category = graphene.Field(CategoryType)
class Arguments:
title = graphene.String()
task = graphene.String()
projId = graphene.Int()
catId = graphene.Int()
def mutate(self, info, title, task, projId, catId):
proj = Project.objects.get(pk=projId)
cat = Category.objects.get(pk=catId)
todo = Todo.objects.create(
title=title,
task=task,
project=proj,
category=cat,
)
return CreateTodo(
id=todo.id,
title=todo.title,
task=todo.task,
slug=todo.slug,
project=todo.project,
category=todo.category,
)
class DeleteTodo(graphene.Mutation):
id = graphene.Int()
title = graphene.String()
ok = graphene.Boolean()
class Arguments:
id = graphene.Int()
def mutate(self, info, id=id):
todo = Todo.objects.get(pk=id)
if todo:
todo.delete()
return DeleteTodo(
id = todo.id,
title = todo.title,
ok = True
)
class UpdateTodo(graphene.Mutation):
id = graphene.Int()
title = graphene.String()
task = graphene.String()
is_completed = graphene.Boolean()
project = graphene.Field(ProjectType)
category = graphene.Field(CategoryType)
slug = graphene.String()
class Arguments:
id = graphene.Int()
title = graphene.String()
task = graphene.String()
is_completed = graphene.Boolean()
projId = graphene.Int()
catId = graphene.Int()
def mutate(self, info, id, title=None, task=None, is_completed=None, projId=None, catId=None):
todo = Todo.objects.get(pk=id)
if projId:
project = Project.objects.get(pk=projId)
if not project:
raise Exception('There is no project with this id.')
if catId:
category = Category.objects.get(pk=catId)
if not category:
raise Exception('No category with this id exists.')
if todo:
if title:
todo.title = title
if task:
todo.task = task
if is_completed:
todo.is_completed = is_completed
if projId:
todo.project = project
if catId:
todo.category = category
todo.save()
return UpdateTodo(
id = todo.id,
title = todo.title,
task = todo.task,
is_completed = todo.is_completed,
project = todo.project,
category = todo.category,
slug = todo.slug,
)
else:
raise Exception('There is no todo with this id.')
class CreateProject(graphene.Mutation):
id = graphene.Int()
name = graphene.String()
description = graphene.String()
slug = graphene.String()
class Arguments:
name = graphene.String()
description = graphene.String()
def mutate(self, info, name, description=None):
project = Project.objects.create(name=name, description=description)
return CreateProject(
id = project.id,
name = project.name,
description = project.description,
slug = project.slug
)
class DeleteProject(graphene.Mutation):
id = graphene.Int()
name = graphene.String()
ok = graphene.Boolean()
class Arguments:
id = graphene.Int()
def mutate(self, info, id=id):
project = Project.objects.get(pk=id)
if project:
project.delete()
return DeleteProject(
id = project.id,
name = project.name,
ok = True,
)
class UpdateProject(graphene.Mutation):
id = graphene.Int()
name = graphene.String()
description = graphene.String()
slug = graphene.String()
class Arguments:
id = graphene.Int()
name = graphene.String()
description = graphene.String()
def mutate(self, info, id=id, name=None, description=None):
project = Project.objects.get(pk=id)
if project:
if name:
project.name = name
if description:
project.description = description
project.save()
return UpdateProject(
id = project.id,
name = project.name,
description = project.description,
slug = project.slug,
)
else:
raise Exception('There is no project with this id.')
class CreateCategory(graphene.Mutation):
id = graphene.Int()
name = graphene.String()
slug = graphene.String()
parent = graphene.Field(CategoryType)
class Arguments:
name = graphene.String()
parentId = graphene.Int()
def mutate(self, info, name, parentId=None):
if parentId:
parent = Category.objects.get(pk=parentId)
category = Category.objects.create(name=name, parent=parent)
else:
category = Category.objects.create(name=name)
return CreateCategory(
id=category.id,
name=category.name,
slug=category.slug,
parent=category.parent,
)
class DeleteCategory(graphene.Mutation):
id = graphene.Int()
name = graphene.String()
slug = graphene.String()
ok = graphene.Boolean()
class Arguments:
id = graphene.Int()
def mutate(self, info, id=id):
category = Category.objects.get(pk=id)
if category:
category.delete()
return DeleteCategory(
id = category.id,
name = category.name,
slug = category.slug,
ok = True
)
class UpdateCategory(graphene.Mutation):
id = graphene.Int()
name = graphene.String()
parent = graphene.Field(CategoryType)
slug = graphene.String()
class Arguments:
id = graphene.Int()
name = graphene.String()
parentId = graphene.Int()
def mutate(self, info, id, name=None, parentId=None):
category = Category.objects.get(pk=id)
if parentId:
parent = Category.objects.get(pk=parentId)
if not parent:
raise Exception('No category with this id to be used as the updated parent.')
if category:
if name:
category.name = name
if parentId:
category.parent = parent
category.save()
return UpdateCategory(
id = category.id,
name = category.name,
parent = category.parent,
slug = category.slug,
)
else:
raise Exception('No category with this id.')
class Mutation(graphene.ObjectType):
create_todo = CreateTodo.Field()
update_todo = UpdateTodo.Field()
delete_todo = DeleteTodo.Field()
create_project = CreateProject.Field()
delete_project = DeleteProject.Field()
update_project = UpdateProject.Field()
create_category = CreateCategory.Field()
delete_category = DeleteCategory.Field()
update_category = UpdateCategory.Field()
schema = Schema(query=Query, mutation=Mutation)
Running my tests…
docker-compose run server pytest

… all 30 of my tests passed. This gives me enormous confidence moving forward with this project.
Next post we add java web token, JWT, user authentication allowing only an authenticated user to create, edit and delete their project todos.