diff --git a/backend/app/crud/tests.py b/backend/app/crud/tests.py
index 112bdc7f88e87fbe0a2c1591b3d5bbfc016a723f..c84cde2d594ec58ea9bd52acae09da18102abe8c 100644
--- a/backend/app/crud/tests.py
+++ b/backend/app/crud/tests.py
@@ -6,7 +6,15 @@ import schemas
 
 
 def create_test(db: Session, test: schemas.TestCreate) -> models.Test:
-    db_test = models.Test(**test.model_dump())
+    db_test = models.Test(**test.model_dump(exclude_unset=True, exclude={"test_task"}))
+
+    if test.test_task:
+        db_test.test_task = models.TestTask(
+            groups=db.query(models.TestTaskGroup)
+            .filter(models.TestTaskGroup.id.in_(test.test_task.groups))
+            .all(),
+        )
+
     db.add(db_test)
     db.commit()
     db.refresh(db_test)
@@ -28,9 +36,19 @@ def update_test(db: Session, test: schemas.TestCreate, test_id: int) -> None:
         ).delete()
 
     if test.test_task:
-        db.query(models.TestTask).filter(models.TestTask.test_id == test_id).update(
-            {**test.test_task.model_dump(exclude_unset=True)}
+
+        test_task = (
+            db.query(models.TestTask).filter(models.TestTask.test_id == test_id).first()
         )
+
+        if test_task:
+            groups = (
+                db.query(models.TestTaskGroup)
+                .filter(models.TestTaskGroup.id.in_(test.test_task.groups))
+                .all()
+            )
+
+            test_task.groups = groups
     else:
         db.query(models.TestTask).filter(models.TestTask.test_id == test_id).delete()
 
@@ -174,3 +192,11 @@ def get_score(db: Session, rid: str):
         return 0
 
     return corrects / total
+
+
+def get_groups(db: Session):
+    return db.query(models.TestTaskGroup).all()
+
+
+def get_questions(db: Session):
+    return db.query(models.TestTaskQuestion).all()
diff --git a/backend/app/routes/tests.py b/backend/app/routes/tests.py
index 6f63bb74646730d476faeae284e01b449407f37b..a8c7c13750dff17402d59517c9a171b76c9ac706 100644
--- a/backend/app/routes/tests.py
+++ b/backend/app/routes/tests.py
@@ -1,6 +1,5 @@
 from fastapi import APIRouter, Depends, HTTPException, status
 from sqlalchemy.orm import Session
-from starlette.status import HTTP_200_OK
 
 import crud
 import schemas
@@ -42,6 +41,22 @@ def get_tests(
     return crud.get_tests(db, skip)
 
 
+@require_admin("You do not have permission to get all the groups.")
+@testRouter.get("/groups", response_model=list[schemas.TestTaskGroup])
+def get_groups(
+    db: Session = Depends(get_db),
+):
+    return crud.get_groups(db)
+
+
+@require_admin("You do not have permission to get all the questions.")
+@testRouter.get("/questions", response_model=list[schemas.TestTaskQuestion])
+def get_questions(
+    db: Session = Depends(get_db),
+):
+    return crud.get_questions(db)
+
+
 @testRouter.get("/{test_id}", response_model=schemas.Test)
 def get_test(
     test_id: int,
diff --git a/backend/app/schemas/tests.py b/backend/app/schemas/tests.py
index fb49e00e88e488519c47bffbdc2de6908aa8fdb5..41ea6af57544bbb2fcb47bcc086752270c95dfdb 100644
--- a/backend/app/schemas/tests.py
+++ b/backend/app/schemas/tests.py
@@ -80,11 +80,11 @@ class TestTaskGroup(TestTaskGroupCreate):
 
 
 class TestTaskCreate(BaseModel):
-    groups: list[TestTaskGroup] = []
+    groups: list[int] = []
 
 
-class TestTask(TestTaskCreate):
-    pass
+class TestTask(BaseModel):
+    groups: list[TestTaskGroup] = []
 
 
 class TestCreate(BaseModel):
diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json
index 7a5dd49695767d0e1161a7e808b6d5c79ed8e452..ce83af49f7e07ea0579102c0f64bd514cdef6b6f 100644
--- a/frontend/src/lang/fr.json
+++ b/frontend/src/lang/fr.json
@@ -13,7 +13,9 @@
 			"users": "Utilisateurs",
 			"sessions": "Sessions",
 			"studies": "Études",
-			"tests": "Tests"
+			"tests": "Tests",
+			"groups": "Groupes",
+			"questions": "Questions"
 		}
 	},
 	"chatbox": {
@@ -414,6 +416,20 @@
 			"text": "Contenu",
 			"repeat": "Nombre de répétitions",
 			"duration": "Durée"
+		},
+		"groups": {
+			"manage": "Gérer les groupes",
+			"create": "Créer un groupe",
+			"backtotests": "Revenir aux tests",
+			"title": {
+				"edit": "Modifier le groupe",
+				"create": "Créer un groupe"
+			}
+		},
+		"questions": {
+			"manage": "Gérer les questions",
+			"create": "Créer une question",
+			"backtogroups": "Revenir aux groupes"
 		}
 	},
 	"button": {
@@ -518,7 +534,16 @@
 			"tests": "tests",
 			"OrganisationUni": "Organisation/Université",
 			"Address": "Adresse",
-			"type": "Type"
+			"type": "Type",
+			"demo": "Demo",
+			"randomize": "Aléatoire",
+			"qcm": "QCM",
+			"image": "Image",
+			"text": "Texte",
+			"audio": "Audio",
+			"dropdown": "Menu déroulant",
+			"radio": "Boutons radio",
+			"gapfill": "Texte à trous"
 		}
 	},
 	"inputs": {
diff --git a/frontend/src/lib/api/tests.ts b/frontend/src/lib/api/tests.ts
index 06e1a9fedcd452fe988f1dcbf6e3e0bd72a609c5..1f4b2e8d1a9b75c21a11f3446f650f756c077e97 100644
--- a/frontend/src/lib/api/tests.ts
+++ b/frontend/src/lib/api/tests.ts
@@ -180,3 +180,63 @@ export async function deleteTestAPI(fetch: fetchType, id: number): Promise<boole
 	if (!response.ok) return false;
 	return true;
 }
+
+export async function getTestAPI(fetch: fetchType, id: number): Promise<any> {
+	const response = await fetch(`/api/tests/${id}`);
+	if (!response.ok) return null;
+	const test = await response.json();
+	return test;
+}
+
+export async function getTestGroupsAPI(fetch: fetchType): Promise<any> {
+	const response = await fetch(`/api/tests/groups`);
+	if (!response.ok) return null;
+	const groups = await response.json();
+	return groups;
+}
+
+export async function getTestQuestionsAPI(fetch: fetchType): Promise<any> {
+	const response = await fetch(`/api/tests/questions`);
+	if (!response.ok) return null;
+	const questions = await response.json();
+	return questions;
+}
+
+export async function createTestTaskAPI(
+	fetch: fetchType,
+	title: string,
+	groups: number[]
+): Promise<number | null> {
+	const response = await fetch(`/api/tests`, {
+		method: 'POST',
+		headers: { 'Content-Type': 'application/json' },
+		body: JSON.stringify({
+			title,
+			test_task: {
+				groups
+			}
+		})
+	});
+	if (!response.ok) return null;
+	const test = await response.json();
+	return test.id;
+}
+
+export async function updateTestTaskAPI(
+	fetch: fetchType,
+	id: number,
+	title: string,
+	groups: number[]
+): Promise<boolean> {
+	const response = await fetch(`/api/tests/${id}`, {
+		method: 'PUT',
+		headers: { 'Content-Type': 'application/json' },
+		body: JSON.stringify({
+			title,
+			test_task: {
+				groups
+			}
+		})
+	});
+	return response.ok;
+}
diff --git a/frontend/src/lib/components/studies/StudyForm.svelte b/frontend/src/lib/components/studies/StudyForm.svelte
index 9cd142beacab75f6f6a645bf5936573211a0f82c..b1b4e255f382bdf8edd05d6f14b1e3877b4a1a5a 100644
--- a/frontend/src/lib/components/studies/StudyForm.svelte
+++ b/frontend/src/lib/components/studies/StudyForm.svelte
@@ -1,6 +1,6 @@
 <script lang="ts">
 	import DateInput from '$lib/components/utils/dateInput.svelte';
-	import Draggable from './Draggable.svelte';
+	import Draggable from '$lib/components/utils/Draggable.svelte';
 	import autosize from 'svelte-autosize';
 	import { toastWarning, toastAlert } from '$lib/utils/toasts';
 	import { getUserByEmailAPI } from '$lib/api/users';
diff --git a/frontend/src/lib/components/tests/GroupForm.svelte b/frontend/src/lib/components/tests/GroupForm.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..67d6856ca90b52b9c2815608eedba21196a8ac7a
--- /dev/null
+++ b/frontend/src/lib/components/tests/GroupForm.svelte
@@ -0,0 +1,125 @@
+<script lang="ts">
+	import { t } from '$lib/services/i18n';
+	import Draggable from '$lib/components/utils/Draggable.svelte';
+	import type TestTaskGroup from '$lib/types/testTaskGroups';
+	import {
+		TestTaskQuestionGapfill,
+		TestTaskQuestionQcm,
+		TestTaskQuestionQcmType,
+		type TestTaskQuestion
+	} from '$lib/types/testTaskQuestions';
+
+	const {
+		group,
+		possibleQuestions,
+		message
+	}: { group: TestTaskGroup | null; possibleQuestions: TestTaskQuestion[]; message: string } =
+		$props();
+
+	let questions = $state(group ? [...group.questions] : []);
+	let selectedQuestion = $state<TestTaskQuestion | null>(null);
+
+	async function deleteGroup() {
+		if (!group) return;
+		await group?.delete();
+		window.location.href = '/admin/tests/group';
+	}
+</script>
+
+<div class="mx-auto w-full max-w-5xl px-4">
+	<h2 class="text-xl font-bold m-5 text-center">
+		{$t(group ? 'tests.groups.title.edit' : 'tests.groups.title.create')}
+	</h2>
+
+	{#if message}
+		<div class="alert alert-error shadow-lg mb-4">
+			{message}
+		</div>
+	{/if}
+
+	<form method="POST">
+		<label for="title" class="label text-sm">{$t('utils.words.title')} *</label>
+		<input
+			type="text"
+			class="input w-full"
+			name="title"
+			id="title"
+			placeholder="Title"
+			value={group?.title}
+			required
+		/>
+
+		<label for="demo" class="label text-sm">{$t('utils.words.demo')}</label>
+		<label class="flex gap-2">
+			{$t('utils.bool.false')}
+			<input type="checkbox" name="demo" id="demo" class="toggle" />
+			{$t('utils.bool.true')}
+		</label>
+
+		<label for="randomize" class="label text-sm">{$t('utils.words.randomize')}</label>
+		<label class="flex gap-2">
+			{$t('utils.bool.false')}
+			<input type="checkbox" checked name="randomize" id="randomize" class="toggle" />
+			{$t('utils.bool.true')}
+		</label>
+
+		<span class="label text-sm capitalize">{$t('utils.words.questions')}</span>
+		<Draggable name="questions[]" bind:items={questions} />
+		<div class="flex max-w-full">
+			<div class="flex-1 min-w-0">
+				<select class="select select-bordered w-full" bind:value={selectedQuestion}>
+					{#each Object.keys(TestTaskQuestionQcmType) as qcmType}
+						<optgroup label={`${$t('utils.words.qcm')} - ${$t(`utils.words.${qcmType}`)}`}>
+							{#each possibleQuestions as question}
+								{#if question instanceof TestTaskQuestionQcm && question.subType === qcmType}
+									<option value={question} title={question.value}>
+										{question.value.substring(0, 120)}
+									</option>
+								{/if}
+							{/each}
+						</optgroup>
+					{/each}
+					<optgroup label={$t('tests.questions.gapfill')}>
+						{#each possibleQuestions as question}
+							{#if question instanceof TestTaskQuestionGapfill}
+								<option value={question} title={question.value}>
+									{question.value.substring(0, 120)}
+								</option>
+							{/if}
+						{/each}
+					</optgroup>
+				</select>
+			</div>
+			<button
+				class="ml-2 button flex-shrink-0"
+				onclick={(e) => {
+					e.preventDefault();
+					if (selectedQuestion === undefined || selectedQuestion === null) return;
+					questions = [...questions, selectedQuestion];
+				}}
+			>
+				+
+			</button>
+		</div>
+
+		<input type="hidden" name="id" value={group?.id} />
+
+		<div class="mt-4 mb-6">
+			<button type="submit" class="button">
+				{group ? $t('button.update') : $t('button.create')}
+			</button>
+			<a class="btn btn-outline float-end ml-2" href="/admin/tests/groups">
+				{$t('button.cancel')}
+			</a>
+			{#if group}
+				<button
+					type="button"
+					class="btn btn-error btn-outline float-end"
+					onclick={() => confirm($t('test.groups.deleteConfirm')) && deleteGroup()}
+				>
+					{$t('button.delete')}
+				</button>
+			{/if}
+		</div>
+	</form>
+</div>
diff --git a/frontend/src/lib/components/tests/TestForm.svelte b/frontend/src/lib/components/tests/TestForm.svelte
index 42ee344c5cea16b3202506ccf540f88a67dbe2e1..62116df4f27720d39b80855eee1f171ce9b54c6e 100644
--- a/frontend/src/lib/components/tests/TestForm.svelte
+++ b/frontend/src/lib/components/tests/TestForm.svelte
@@ -1,11 +1,19 @@
 <script lang="ts">
-	import type { Test, TestTyping } from '$lib/types/tests';
+	import { TestTask, type Test, type TestTyping } from '$lib/types/tests';
 	import { t } from '$lib/services/i18n';
 	import autosize from 'svelte-autosize';
+	import Draggable from '$lib/components/utils/Draggable.svelte';
+	import type TestTaskGroup from '$lib/types/testTaskGroups';
 
-	const { test }: { test: Test | null } = $props();
+	const {
+		test,
+		possibleGroups,
+		message
+	}: { test: Test | null; possibleGroups: TestTaskGroup[]; message: string } = $props();
 
 	let type = $state(test?.type);
+	let groups = $state(test && test instanceof TestTask ? [...test.groups] : []);
+	let selectedGroup = $state<TestTaskGroup | null>(null);
 
 	async function deleteTest() {
 		if (!test) return;
@@ -19,6 +27,12 @@
 		{$t(test ? 'tests.title.edit' : 'tests.title.create')}
 	</h2>
 
+	{#if message}
+		<div class="alert alert-error shadow-lg mb-4">
+			{message}
+		</div>
+	{/if}
+
 	<form method="POST">
 		<label for="title" class="label text-sm">{$t('tests.label.title')} *</label>
 		<input
@@ -79,7 +93,29 @@
 				min="0"
 				value={testTyping?.duration || 60}
 			/>
-		{:else}{/if}
+		{:else if type === 'task'}
+			<h3 class="py-2 px-1 capitalize">{$t('utils.words.groups')}</h3>
+			<Draggable name="groups[]" bind:items={groups} />
+			<div class="flex">
+				<select class="select select-bordered flex-grow" bind:value={selectedGroup}>
+					{#each possibleGroups as group}
+						<option value={group}>
+							{group.title} - {group.questions.length} questions
+						</option>
+					{/each}
+				</select>
+				<button
+					class="ml-2 button"
+					onclick={(e) => {
+						e.preventDefault();
+						if (selectedGroup === undefined || selectedGroup === null) return;
+						groups = [...groups, selectedGroup];
+					}}
+				>
+					+
+				</button>
+			</div>
+		{/if}
 
 		<input type="hidden" name="id" value={test?.id} />
 
diff --git a/frontend/src/lib/components/studies/Draggable.svelte b/frontend/src/lib/components/utils/Draggable.svelte
similarity index 87%
rename from frontend/src/lib/components/studies/Draggable.svelte
rename to frontend/src/lib/components/utils/Draggable.svelte
index 95fc21e0e99ec7800650decb359e9ce76a15658b..6f8ca8ac284dd9405202d7f6e1e86d9fdb945f4a 100644
--- a/frontend/src/lib/components/studies/Draggable.svelte
+++ b/frontend/src/lib/components/utils/Draggable.svelte
@@ -1,6 +1,8 @@
 <script lang="ts">
 	import { t } from '$lib/services/i18n';
 	import { TestTask, TestTyping } from '$lib/types/tests';
+	import TestTaskGroup from '$lib/types/testTaskGroups';
+	import { TestTaskQuestionGapfill, TestTaskQuestionQcm } from '$lib/types/testTaskQuestions';
 
 	let { items = $bindable(), name } = $props();
 	let draggedIndex: number | null = $state(null);
@@ -54,6 +56,14 @@
 					{$t('utils.words.questions')})
 				{:else if item instanceof TestTyping}
 					{item.title}
+				{:else if item instanceof TestTaskGroup}
+					{item.title}
+					({item.questions.length}
+					{$t('utils.words.questions')})
+				{:else if item instanceof TestTaskQuestionQcm}
+					{@html item.value}
+				{:else if item instanceof TestTaskQuestionGapfill}
+					{item.value}
 				{/if}
 			</div>
 			<div
diff --git a/frontend/src/lib/types/testTaskQuestions.ts b/frontend/src/lib/types/testTaskQuestions.ts
index afa027fa27cd563015c74d5b87edfbedf18b211c..2a2860fe6dfd672f0590de0ce699fc983500c310 100644
--- a/frontend/src/lib/types/testTaskQuestions.ts
+++ b/frontend/src/lib/types/testTaskQuestions.ts
@@ -17,6 +17,15 @@ export abstract class TestTaskQuestion {
 		return this._question;
 	}
 
+	get type(): string {
+		if (this instanceof TestTaskQuestionQcm) {
+			return 'qcm';
+		} else if (this instanceof TestTaskQuestionGapfill) {
+			return 'gapfill';
+		}
+		return 'unknown';
+	}
+
 	static parse(data: any): TestTaskQuestion | null {
 		if (data === null) {
 			return null;
@@ -76,7 +85,7 @@ export class TestTaskQuestionQcm extends TestTaskQuestion {
 		return this._correct;
 	}
 
-	get type(): TestTaskQuestionQcmType | null {
+	get subType(): TestTaskQuestionQcmType | null {
 		switch (this.question.split(':')[0]) {
 			case 'image':
 				return TestTaskQuestionQcmType.image;
@@ -138,6 +147,10 @@ export class TestTaskQuestionGapfill extends TestTaskQuestion {
 		return parts;
 	}
 
+	get value(): string {
+		return super.question.split(':').slice(1).join(':');
+	}
+
 	static parse(data: any): TestTaskQuestionGapfill | null {
 		if (data === null) {
 			return null;
diff --git a/frontend/src/routes/admin/tests/+page.svelte b/frontend/src/routes/admin/tests/+page.svelte
index 7c1f6fa3b9e36545713b6a9880dd6874addc2c0e..117b2364d0765181caecf653b0990867d96174a9 100644
--- a/frontend/src/routes/admin/tests/+page.svelte
+++ b/frontend/src/routes/admin/tests/+page.svelte
@@ -33,5 +33,8 @@
 </table>
 <div class="mt-8 mx-auto w-[64rem] flex justify-between">
 	<a class="button" href="/admin/tests/new">{$t('tests.create')}</a>
-	<a class="btn" href="/admin/studies">⏎ {$t('tests.backtostudies')}</a>
+	<span>
+		<a class="btn" href="/admin/studies">⏎ {$t('tests.backtostudies')}</a>
+		<a class="btn" href="/admin/tests/groups">{$t('tests.groups.manage')}</a>
+	</span>
 </div>
diff --git a/frontend/src/routes/admin/tests/[id]/+page.server.ts b/frontend/src/routes/admin/tests/[id]/+page.server.ts
index 5ecbb67f23bc3139a80d8d2a2cfb3eede42a23a7..9305175ac90ffa43b2eca215bfb70ed98938108f 100644
--- a/frontend/src/routes/admin/tests/[id]/+page.server.ts
+++ b/frontend/src/routes/admin/tests/[id]/+page.server.ts
@@ -1,5 +1,5 @@
 import { redirect, type Actions } from '@sveltejs/kit';
-import { updateTestTypingAPI } from '$lib/api/tests';
+import { updateTestTaskAPI, updateTestTypingAPI } from '$lib/api/tests';
 
 export const actions: Actions = {
 	default: async ({ request, fetch }) => {
@@ -52,6 +52,21 @@ export const actions: Actions = {
 				};
 			}
 
+			return redirect(303, `/admin/tests`);
+		} else if (type === 'task') {
+			const groups = formData
+				.getAll('groups[]')
+				.map((group) => parseInt(group.toString(), 10))
+				.filter((group) => !isNaN(group));
+
+			const ok = await updateTestTaskAPI(fetch, id, title, groups);
+
+			if (!ok) {
+				return {
+					message: 'Invalid request: Failed to update test'
+				};
+			}
+
 			return redirect(303, `/admin/tests`);
 		}
 	}
diff --git a/frontend/src/routes/admin/tests/[id]/+page.svelte b/frontend/src/routes/admin/tests/[id]/+page.svelte
index d3e6ef7c67755d69e05d3e82fc5cdbb2a12b649c..d6f2975165bf05074da7f689bbb105032079105f 100644
--- a/frontend/src/routes/admin/tests/[id]/+page.svelte
+++ b/frontend/src/routes/admin/tests/[id]/+page.svelte
@@ -2,8 +2,8 @@
 	import TestForm from '$lib/components/tests/TestForm.svelte';
 	import type { PageData } from './$types';
 
-	const { data }: { data: PageData } = $props();
-	const { test } = data;
+	const { data, form }: { data: PageData; form: FormData } = $props();
+	const { test, possibleGroups } = data;
 </script>
 
-<TestForm {test} />
+<TestForm {test} {possibleGroups} message={form?.message} />
diff --git a/frontend/src/routes/admin/tests/[id]/+page.ts b/frontend/src/routes/admin/tests/[id]/+page.ts
index 71076a850d6299a3784de794ffdd803c240baa2c..c49f9ecbdebc98feff80f8cb59ee5c82d38e1f90 100644
--- a/frontend/src/routes/admin/tests/[id]/+page.ts
+++ b/frontend/src/routes/admin/tests/[id]/+page.ts
@@ -1,4 +1,6 @@
+import { getTestAPI, getTestGroupsAPI } from '$lib/api/tests';
 import { Test } from '$lib/types/tests';
+import TestTaskGroup from '$lib/types/testTaskGroups';
 import { error, type Load } from '@sveltejs/kit';
 
 export const load: Load = async ({ fetch, params }) => {
@@ -8,8 +10,11 @@ export const load: Load = async ({ fetch, params }) => {
 		return error(400, 'Invalid ID');
 	}
 
-	const testRaw = await (await fetch(`/api/tests/${id}`)).json();
+	const testRaw = await getTestAPI(fetch, id);
 	const test = Test.parse(testRaw);
 
-	return { test };
+	const groupsRaw = await getTestGroupsAPI(fetch);
+	const groups = TestTaskGroup.parseAll(groupsRaw);
+
+	return { test, possibleGroups: groups };
 };
diff --git a/frontend/src/routes/admin/tests/groups/+page.svelte b/frontend/src/routes/admin/tests/groups/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..b1b79de37821cf205a2722de992f2c8bb32b6b93
--- /dev/null
+++ b/frontend/src/routes/admin/tests/groups/+page.svelte
@@ -0,0 +1,44 @@
+<script lang="ts">
+	import { t } from '$lib/services/i18n';
+	import type { PageData } from './$types';
+	import type TestTaskGroup from '$lib/types/testTaskGroups';
+
+	const { data }: { data: PageData } = $props();
+
+	let groups: TestTaskGroup[] = $state(data.groups);
+</script>
+
+<h1 class="text-xl font-bold m-5 text-center">{$t('header.admin.groups')}</h1>
+
+<table class="table max-w-5xl mx-auto text-left">
+	<thead>
+		<tr>
+			<th>#</th>
+			<th>{$t('utils.words.title')}</th>
+			<th>{$t('utils.words.demo')}</th>
+			<th>{$t('utils.words.randomize')}</th>
+			<th class="capitalize"># {$t('utils.words.questions')}</th>
+		</tr>
+	</thead>
+	<tbody>
+		{#each groups as group (group.id)}
+			<tr
+				class="hover:bg-gray-100 hover:cursor-pointer"
+				onclick={() => (window.location.href = `/admin/tests/groups/${group.id}`)}
+			>
+				<td>{group.id}</td>
+				<td>{group.title}</td>
+				<td>{$t(`utils.bool.${group.demo}`)}</td>
+				<td>{$t(`utils.bool.${group.randomize}`)}</td>
+				<td>{group.questions.length}</td>
+			</tr>
+		{/each}
+	</tbody>
+</table>
+<div class="mt-8 mx-auto w-[64rem] flex justify-between">
+	<a class="button" href="/admin/tests/groups/new">{$t('tests.groups.create')}</a>
+	<span>
+		<a class="btn" href="/admin/tests">⏎ {$t('tests.groups.backtotests')}</a>
+		<a class="btn" href="/admin/tests/groups/questions">{$t('tests.questions.manage')}</a>
+	</span>
+</div>
diff --git a/frontend/src/routes/admin/tests/groups/+page.ts b/frontend/src/routes/admin/tests/groups/+page.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c6103e67cc138f5fe092cc706dd922ed58f7e5f9
--- /dev/null
+++ b/frontend/src/routes/admin/tests/groups/+page.ts
@@ -0,0 +1,12 @@
+import { getTestGroupsAPI } from '$lib/api/tests';
+import { type Load } from '@sveltejs/kit';
+import TestTaskGroup from '$lib/types/testTaskGroups';
+
+export const load: Load = async ({ fetch }) => {
+	const groupsRaw = await getTestGroupsAPI(fetch);
+	const groups = TestTaskGroup.parseAll(groupsRaw);
+
+	return {
+		groups
+	};
+};
diff --git a/frontend/src/routes/admin/tests/groups/[id]/+page.server.ts b/frontend/src/routes/admin/tests/groups/[id]/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5ecbb67f23bc3139a80d8d2a2cfb3eede42a23a7
--- /dev/null
+++ b/frontend/src/routes/admin/tests/groups/[id]/+page.server.ts
@@ -0,0 +1,58 @@
+import { redirect, type Actions } from '@sveltejs/kit';
+import { updateTestTypingAPI } from '$lib/api/tests';
+
+export const actions: Actions = {
+	default: async ({ request, fetch }) => {
+		const formData = await request.formData();
+
+		const idStr = formData.get('id')?.toString();
+		const title = formData.get('title')?.toString();
+		const type = formData.get('type')?.toString();
+
+		if (!title || !type || !idStr || (type !== 'typing' && type !== 'task')) {
+			return {
+				message: 'Invalid request: Missing required fields'
+			};
+		}
+
+		const id = parseInt(idStr, 10);
+
+		if (isNaN(id)) {
+			return {
+				message: 'Invalid request: Invalid ID'
+			};
+		}
+
+		if (type === 'typing') {
+			const explanation = formData.get('explanation')?.toString();
+			const text = formData.get('text')?.toString();
+			const repeatStr = formData.get('repeat')?.toString();
+			const durationStr = formData.get('duration')?.toString();
+
+			if (!explanation || !text || !repeatStr || !durationStr) {
+				return {
+					message: 'Invalid request: Missing required fields'
+				};
+			}
+
+			const repeat = parseInt(repeatStr, 10);
+			const duration = parseInt(durationStr, 10);
+
+			if (isNaN(repeat) || isNaN(duration)) {
+				return {
+					message: 'Invalid request: Invalid format for numbers'
+				};
+			}
+
+			const ok = await updateTestTypingAPI(fetch, id, title, explanation, text, repeat, duration);
+
+			if (!ok) {
+				return {
+					message: 'Invalid request: Failed to update test'
+				};
+			}
+
+			return redirect(303, `/admin/tests`);
+		}
+	}
+};
diff --git a/frontend/src/routes/admin/tests/groups/[id]/+page.svelte b/frontend/src/routes/admin/tests/groups/[id]/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..97ba7436319082212d3affbc96851cfd510d15bb
--- /dev/null
+++ b/frontend/src/routes/admin/tests/groups/[id]/+page.svelte
@@ -0,0 +1,9 @@
+<script lang="ts">
+	import TestForm from '$lib/components/tests/TestForm.svelte';
+	import type { PageData } from './$types';
+
+	const { data }: { data: PageData } = $props();
+	const { test, possibleGroups } = data;
+</script>
+
+<TestForm {test} {possibleGroups} />
diff --git a/frontend/src/routes/admin/tests/groups/[id]/+page.ts b/frontend/src/routes/admin/tests/groups/[id]/+page.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c49f9ecbdebc98feff80f8cb59ee5c82d38e1f90
--- /dev/null
+++ b/frontend/src/routes/admin/tests/groups/[id]/+page.ts
@@ -0,0 +1,20 @@
+import { getTestAPI, getTestGroupsAPI } from '$lib/api/tests';
+import { Test } from '$lib/types/tests';
+import TestTaskGroup from '$lib/types/testTaskGroups';
+import { error, type Load } from '@sveltejs/kit';
+
+export const load: Load = async ({ fetch, params }) => {
+	const id = Number(params.id);
+
+	if (isNaN(id)) {
+		return error(400, 'Invalid ID');
+	}
+
+	const testRaw = await getTestAPI(fetch, id);
+	const test = Test.parse(testRaw);
+
+	const groupsRaw = await getTestGroupsAPI(fetch);
+	const groups = TestTaskGroup.parseAll(groupsRaw);
+
+	return { test, possibleGroups: groups };
+};
diff --git a/frontend/src/routes/admin/tests/groups/new/+page.server.ts b/frontend/src/routes/admin/tests/groups/new/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2fe5e2218cd89c20a8fac9bfa9c8eec46bd4e196
--- /dev/null
+++ b/frontend/src/routes/admin/tests/groups/new/+page.server.ts
@@ -0,0 +1,27 @@
+import { redirect, type Actions } from '@sveltejs/kit';
+import { createTestTypingAPI } from '$lib/api/tests';
+
+export const actions: Actions = {
+	default: async ({ request, fetch }) => {
+		const formData = await request.formData();
+
+		const title = formData.get('title')?.toString();
+		const demo = formData.get('demo')?.toString() == 'on';
+		const randomize = formData.get('randomize')?.toString() == 'on';
+
+		if (!title || !demo || !randomize) {
+			return {
+				message: 'Invalid request: Missing required fields'
+			};
+		}
+
+		const questions = formData
+			.getAll('questions[]')
+			.map((question) => parseInt(question.toString(), 10))
+			.filter((question) => !isNaN(question));
+
+		const id = await createTestTaskGroupAPI(fetch, title, demo, randomize, questions);
+
+		return redirect(303, `/admin/tests`);
+	}
+};
diff --git a/frontend/src/routes/admin/tests/groups/new/+page.svelte b/frontend/src/routes/admin/tests/groups/new/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..4c3987e3c76401719759a7f0c44789e9f314512c
--- /dev/null
+++ b/frontend/src/routes/admin/tests/groups/new/+page.svelte
@@ -0,0 +1,7 @@
+<script lang="ts">
+	import GroupForm from '$lib/components/tests/GroupForm.svelte';
+	const { data, form }: { data: PageData; form: FormData } = $props();
+	const { possibleQuestions } = data;
+</script>
+
+<GroupForm group={null} {possibleQuestions} message={form?.message} />
diff --git a/frontend/src/routes/admin/tests/groups/new/+page.ts b/frontend/src/routes/admin/tests/groups/new/+page.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fcf5f18b9cacebac5116425d6d3c1619cc698065
--- /dev/null
+++ b/frontend/src/routes/admin/tests/groups/new/+page.ts
@@ -0,0 +1,10 @@
+import { getTestQuestionsAPI } from '$lib/api/tests';
+import { TestTaskQuestion } from '$lib/types/testTaskQuestions';
+import { type Load } from '@sveltejs/kit';
+
+export const load: Load = async ({ fetch }) => {
+	const questionsRaw = await getTestQuestionsAPI(fetch);
+	const questions = TestTaskQuestion.parseAll(questionsRaw);
+
+	return { possibleQuestions: questions };
+};
diff --git a/frontend/src/routes/admin/tests/new/+page.server.ts b/frontend/src/routes/admin/tests/new/+page.server.ts
index fd629c3d9571a485ee5e974abcef3f6fe785dded..7cc2b4f5f13481357e10eb15d44616aee216b67e 100644
--- a/frontend/src/routes/admin/tests/new/+page.server.ts
+++ b/frontend/src/routes/admin/tests/new/+page.server.ts
@@ -1,5 +1,5 @@
 import { redirect, type Actions } from '@sveltejs/kit';
-import { createTestTypingAPI } from '$lib/api/tests';
+import { createTestTaskAPI, createTestTypingAPI } from '$lib/api/tests';
 
 export const actions: Actions = {
 	default: async ({ request, fetch }) => {
@@ -43,6 +43,21 @@ export const actions: Actions = {
 				};
 			}
 
+			return redirect(303, `/admin/tests`);
+		} else if (type === 'task') {
+			const groups = formData
+				.getAll('groups[]')
+				.map((group) => parseInt(group.toString(), 10))
+				.filter((group) => !isNaN(group));
+
+			const id = await createTestTaskAPI(fetch, title, groups);
+
+			if (id === null) {
+				return {
+					message: 'Invalid request: Failed to create test'
+				};
+			}
+
 			return redirect(303, `/admin/tests`);
 		}
 	}
diff --git a/frontend/src/routes/admin/tests/new/+page.svelte b/frontend/src/routes/admin/tests/new/+page.svelte
index e62bf17d7b045c4057ff603f653be1ccd15be49f..7cdcedfb06d7f392cad0af12dc07966f558c88a2 100644
--- a/frontend/src/routes/admin/tests/new/+page.svelte
+++ b/frontend/src/routes/admin/tests/new/+page.svelte
@@ -1,5 +1,7 @@
 <script lang="ts">
 	import TestForm from '$lib/components/tests/TestForm.svelte';
+	const { data, form }: { data: PageData; form: FormData } = $props();
+	const { possibleGroups } = data;
 </script>
 
-<TestForm test={null} />
+<TestForm test={null} {possibleGroups} message={form?.message} />
diff --git a/frontend/src/routes/admin/tests/new/+page.ts b/frontend/src/routes/admin/tests/new/+page.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f56d77dafbe4bdee1d89ce69ff99a0528057f5df
--- /dev/null
+++ b/frontend/src/routes/admin/tests/new/+page.ts
@@ -0,0 +1,10 @@
+import { getTestGroupsAPI } from '$lib/api/tests';
+import TestTaskGroup from '$lib/types/testTaskGroups';
+import { type Load } from '@sveltejs/kit';
+
+export const load: Load = async ({ fetch }) => {
+	const groupsRaw = await getTestGroupsAPI(fetch);
+	const groups = TestTaskGroup.parseAll(groupsRaw);
+
+	return { possibleGroups: groups };
+};