diff --git a/dev/README.md b/dev/README.md
index 41bb69a1826796762fc13908985067f1a57b8ea6..b5fff413a2ef228f2d8f61529cff44992fd9003e 100644
--- a/dev/README.md
+++ b/dev/README.md
@@ -141,3 +141,11 @@ The format of the changelog entries should be:
 
 To determine how to raise the version, read the versioning section [here](../definitions/README.md#versioning).
 
+
+### Adding tests
+
+Adding new test cases requires a lot of boring boilerplate code to set up.
+For this reason, we have a [templates](templates) directory,
+which contains templates that contain _most_ of the code necessary to get going.
+In most cases, they actually contain too much setup,
+which should be removed when unnecessary.
diff --git a/dev/templates/api_test_case.template b/dev/templates/api_test_case.template
new file mode 100644
index 0000000000000000000000000000000000000000..8631104933723c9e8abe7745bdaa7e4c2d1d9504
--- /dev/null
+++ b/dev/templates/api_test_case.template
@@ -0,0 +1,66 @@
+import os
+import shutil
+
+from django.conf import settings
+from django.test import override_settings
+
+from common_lib.graphql import GraphQLApiTestCase
+from common_lib.test_utils import (
+    internal_upload_definition,
+    internal_create_exercise,
+)
+from exercise.models import Exercise
+from exercise_definition.models import Definition
+from user.models import User
+
+TEST_DATA_STORAGE = "RENAME_ME"
+
+
+@override_settings(
+    DATA_STORAGE=TEST_DATA_STORAGE,
+    FILE_STORAGE=os.path.join(TEST_DATA_STORAGE, "files"),
+)
+class RENAME_ME(GraphQLApiTestCase):
+    base_definition: Definition
+    email_definition: Definition
+    roles_definition: Definition
+
+    base_exercise: Exercise
+    email_exercise: Exercise
+    roles_exercise: Exercise
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.base_definition = internal_upload_definition("base_definition")
+        cls.email_definition = internal_upload_definition("email_definition")
+        cls.roles_definition = internal_upload_definition("roles_definition")
+
+        cls.base_exercise = internal_create_exercise(cls.base_definition.id, 2)
+        cls.email_exercise = internal_create_exercise(
+            cls.email_definition.id, 2
+        )
+        cls.roles_exercise = internal_create_exercise(
+            cls.roles_definition.id, 3
+        )
+
+        cls.instructor = User.objects.create_staffuser(
+            "instructor@instructor.com", "instructor"
+        )
+        cls.user = User.objects.create_user("user@user.com", "user")
+
+        cls.instructor.definitions.add(cls.base_definition)
+        cls.instructor.definitions.add(cls.email_definition)
+        cls.instructor.definitions.add(cls.roles_definition)
+
+        cls.instructor.exercises.add(cls.base_exercise)
+        cls.instructor.exercises.add(cls.email_exercise)
+        cls.instructor.exercises.add(cls.roles_exercise)
+
+        cls.user.teams.add(cls.base_exercise.teams.first())
+        cls.user.teams.add(cls.email_exercise.teams.first())
+        cls.user.teams.add(cls.roles_exercise.teams.first())
+
+    @classmethod
+    def tearDownClass(cls):
+        shutil.rmtree(settings.DATA_STORAGE)
+        super().tearDownClass()
diff --git a/dev/templates/generic_test_case.template b/dev/templates/generic_test_case.template
new file mode 100644
index 0000000000000000000000000000000000000000..5404ec19127d266a6aaeefdc4ebf5db928df14a7
--- /dev/null
+++ b/dev/templates/generic_test_case.template
@@ -0,0 +1,47 @@
+import os
+import shutil
+
+from django.conf import settings
+from django.test import TestCase, override_settings
+
+from common_lib.test_utils import (
+    internal_upload_definition,
+    internal_create_exercise,
+)
+from exercise.models import Exercise
+from exercise_definition.models import Definition
+
+TEST_DATA_STORAGE = "RENAME_ME"
+
+
+@override_settings(
+    DATA_STORAGE=TEST_DATA_STORAGE,
+    FILE_STORAGE=os.path.join(TEST_DATA_STORAGE, "files"),
+)
+class RENAME_ME(TestCase):
+    base_definition: Definition
+    email_definition: Definition
+    roles_definition: Definition
+
+    base_exercise: Exercise
+    email_exercise: Exercise
+    roles_exercise: Exercise
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.base_definition = internal_upload_definition("base_definition")
+        cls.email_definition = internal_upload_definition("email_definition")
+        cls.roles_definition = internal_upload_definition("roles_definition")
+
+        cls.base_exercise = internal_create_exercise(cls.base_definition.id, 2)
+        cls.email_exercise = internal_create_exercise(
+            cls.email_definition.id, 2
+        )
+        cls.roles_exercise = internal_create_exercise(
+            cls.roles_definition.id, 3
+        )
+
+    @classmethod
+    def tearDownClass(cls):
+        shutil.rmtree(settings.DATA_STORAGE)
+        super().tearDownClass()