diff --git a/backend/app/crud/tests.py b/backend/app/crud/tests.py
index d3297ff054e327a2e574ae72727dff7309defa40..112bdc7f88e87fbe0a2c1591b3d5bbfc016a723f 100644
--- a/backend/app/crud/tests.py
+++ b/backend/app/crud/tests.py
@@ -13,6 +13,30 @@ def create_test(db: Session, test: schemas.TestCreate) -> models.Test:
     return db_test
 
 
+def update_test(db: Session, test: schemas.TestCreate, test_id: int) -> None:
+    db.query(models.Test).filter(models.Test.id == test_id).update(
+        {**test.model_dump(exclude_unset=True, exclude={"test_typing", "test_task"})}
+    )
+
+    if test.test_typing:
+        db.query(models.TestTyping).filter(models.TestTyping.test_id == test_id).update(
+            {**test.test_typing.model_dump(exclude_unset=True)}
+        )
+    else:
+        db.query(models.TestTyping).filter(
+            models.TestTyping.test_id == test_id
+        ).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)}
+        )
+    else:
+        db.query(models.TestTask).filter(models.TestTask.test_id == test_id).delete()
+
+    db.commit()
+
+
 def get_tests(db: Session, skip: int = 0) -> list[models.Test]:
     return db.query(models.Test).offset(skip).all()
 
diff --git a/backend/app/routes/tests.py b/backend/app/routes/tests.py
index f310d546cfad88c57636aad32c2d14aaa6ac4c26..6f63bb74646730d476faeae284e01b449407f37b 100644
--- a/backend/app/routes/tests.py
+++ b/backend/app/routes/tests.py
@@ -19,6 +19,21 @@ def create_test(
     return crud.create_test(db, test).id
 
 
+@require_admin("You do not have permission to update a test.")
+@testRouter.put("/{test_id}", status_code=status.HTTP_204_NO_CONTENT)
+def update_test(
+    test_id: int,
+    test: schemas.TestCreate,
+    db: Session = Depends(get_db),
+):
+    db_test = crud.get_test(db, test_id)
+    if db_test is None:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND, detail="Test not found"
+        )
+    crud.update_test(db, test, test_id)
+
+
 @testRouter.get("")
 def get_tests(
     skip: int = 0,
diff --git a/frontend/src/lang/fr.json b/frontend/src/lang/fr.json
index 8136121e3ac9a7486fa2913320c466af4a30731c..7a5dd49695767d0e1161a7e808b6d5c79ed8e452 100644
--- a/frontend/src/lang/fr.json
+++ b/frontend/src/lang/fr.json
@@ -12,7 +12,8 @@
 		"admin": {
 			"users": "Utilisateurs",
 			"sessions": "Sessions",
-			"studies": "Études"
+			"studies": "Études",
+			"tests": "Tests"
 		}
 	},
 	"chatbox": {
@@ -398,7 +399,22 @@
 	},
 	"tests": {
 		"taskTests": "Tests de langue",
-		"typingTests": "Tests de frappe"
+		"typingTests": "Tests de frappe",
+		"manage": "Gerer les tests",
+		"create": "Créer un nouveau test",
+		"backtostudies": "Revenir aux études",
+		"title": {
+			"edit": "Modifier le test",
+			"create": "Créer un test"
+		},
+		"label": {
+			"title": "Titre",
+			"type": "Type",
+			"explanation": "Explication",
+			"text": "Contenu",
+			"repeat": "Nombre de répétitions",
+			"duration": "Durée"
+		}
 	},
 	"button": {
 		"create": "Créer",
@@ -492,7 +508,7 @@
 			"inProgress": "En cours",
 			"finished": "Terminée",
 			"topics": "Topics",
-			"title": "Titre de l'étude",
+			"title": "Titre",
 			"users": "Utilisateurs",
 			"description": "Description",
 			"email": "E-mail",
@@ -501,7 +517,8 @@
 			"questions": "questions",
 			"tests": "tests",
 			"OrganisationUni": "Organisation/Université",
-			"Address": "Adresse"
+			"Address": "Adresse",
+			"type": "Type"
 		}
 	},
 	"inputs": {
diff --git a/frontend/src/lib/api/tests.ts b/frontend/src/lib/api/tests.ts
index 54a3fe1f30bfb9e834da7419f01cf39db36a4af0..06e1a9fedcd452fe988f1dcbf6e3e0bd72a609c5 100644
--- a/frontend/src/lib/api/tests.ts
+++ b/frontend/src/lib/api/tests.ts
@@ -120,3 +120,63 @@ export async function getTestEntriesScoreAPI(
 
 	return score;
 }
+
+export async function createTestTypingAPI(
+	fetch: fetchType,
+	title: string,
+	explanation: string,
+	text: string,
+	repeat: number,
+	duration: number
+) {
+	const response = await fetch(`/api/tests`, {
+		method: 'POST',
+		headers: { 'Content-Type': 'application/json' },
+		body: JSON.stringify({
+			title,
+			test_typing: {
+				explanations: explanation,
+				text,
+				repeat,
+				duration
+			}
+		})
+	});
+	if (!response.ok) return null;
+	const test = await response.json();
+	return test.id;
+}
+
+export async function updateTestTypingAPI(
+	fetch: fetchType,
+	id: number,
+	title: string,
+	explanation: string,
+	text: string,
+	repeat: number,
+	duration: number
+): Promise<boolean> {
+	const response = await fetch(`/api/tests/${id}`, {
+		method: 'PUT',
+		headers: { 'Content-Type': 'application/json' },
+		body: JSON.stringify({
+			title,
+			test_typing: {
+				explanations: explanation,
+				text,
+				repeat,
+				duration
+			}
+		})
+	});
+
+	return response.ok;
+}
+
+export async function deleteTestAPI(fetch: fetchType, id: number): Promise<boolean> {
+	const response = await fetch(`/api/tests/${id}`, {
+		method: 'DELETE'
+	});
+	if (!response.ok) return false;
+	return true;
+}
diff --git a/frontend/src/lib/components/tests/TestForm.svelte b/frontend/src/lib/components/tests/TestForm.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..42ee344c5cea16b3202506ccf540f88a67dbe2e1
--- /dev/null
+++ b/frontend/src/lib/components/tests/TestForm.svelte
@@ -0,0 +1,104 @@
+<script lang="ts">
+	import type { Test, TestTyping } from '$lib/types/tests';
+	import { t } from '$lib/services/i18n';
+	import autosize from 'svelte-autosize';
+
+	const { test }: { test: Test | null } = $props();
+
+	let type = $state(test?.type);
+
+	async function deleteTest() {
+		if (!test) return;
+		await test?.delete();
+		window.location.href = '/admin/tests';
+	}
+</script>
+
+<div class="mx-auto w-full max-w-5xl px-4">
+	<h2 class="text-xl font-bold m-5 text-center">
+		{$t(test ? 'tests.title.edit' : 'tests.title.create')}
+	</h2>
+
+	<form method="POST">
+		<label for="title" class="label text-sm">{$t('tests.label.title')} *</label>
+		<input
+			type="text"
+			class="input w-full"
+			name="title"
+			id="title"
+			placeholder="Title"
+			value={test?.title}
+			required
+		/>
+
+		<label for="type" class="label text-sm">{$t('tests.label.type')} *</label>
+		<select name="type" id="type" class="select w-full input" bind:value={type}>
+			<option value="typing">Typing Test</option>
+			<option value="task">Task</option>
+		</select>
+
+		{#if type === 'typing'}
+			{@const testTyping = test as TestTyping | null}
+			<label for="explanation" class="label text-sm">{$t('tests.label.explanation')} *</label>
+			<input
+				class="input w-full"
+				type="text"
+				name="explanation"
+				id="explanation"
+				placeholder="Explanation"
+				value={testTyping?.explanations}
+				required
+			/>
+			<label for="text" class="label text-sm">{$t('tests.label.text')} *</label>
+			<textarea
+				use:autosize
+				class="input w-full max-h-52"
+				id="text"
+				name="text"
+				placeholder="Text to type"
+				value={testTyping?.text}
+				required
+			></textarea>
+			<label for="repeat" class="label text-sm">{$t('tests.label.repeat')} *</label>
+			<input
+				type="number"
+				class="input w-full"
+				name="repeat"
+				id="repeat"
+				required
+				min="1"
+				value={testTyping?.repeat || 1}
+			/>
+			<label for="duration" class="label text-sm">{$t('tests.label.duration')} *</label>
+			<input
+				type="number"
+				class="input w-full"
+				name="duration"
+				id="duration"
+				required
+				min="0"
+				value={testTyping?.duration || 60}
+			/>
+		{:else}{/if}
+
+		<input type="hidden" name="id" value={test?.id} />
+
+		<div class="mt-4 mb-6">
+			<button type="submit" class="button" disabled={!type}>
+				{test ? $t('button.update') : $t('button.create')}
+			</button>
+			<a class="btn btn-outline float-end ml-2" href="/admin/tests">
+				{$t('button.cancel')}
+			</a>
+			{#if test}
+				<button
+					type="button"
+					class="btn btn-error btn-outline float-end"
+					onclick={() => confirm($t('test.deleteConfirm')) && deleteTest()}
+				>
+					{$t('button.delete')}
+				</button>
+			{/if}
+		</div>
+	</form>
+</div>
diff --git a/frontend/src/lib/types/tests.ts b/frontend/src/lib/types/tests.ts
index e71ef0a1487686893530609b064a498fafabd51b..c59d9f48ffdafa0c2f5f8526524fe820f340a4eb 100644
--- a/frontend/src/lib/types/tests.ts
+++ b/frontend/src/lib/types/tests.ts
@@ -1,4 +1,6 @@
+import { deleteTestAPI } from '$lib/api/tests';
 import { toastAlert } from '$lib/utils/toasts';
+import type { fetchType } from '$lib/utils/types';
 import TestTaskGroup from './testTaskGroups';
 
 export abstract class Test {
@@ -18,6 +20,19 @@ export abstract class Test {
 		return this._title;
 	}
 
+	get type(): string {
+		if (this instanceof TestTask) {
+			return 'task';
+		} else if (this instanceof TestTyping) {
+			return 'typing';
+		}
+		return 'unknown';
+	}
+
+	async delete(f: fetchType = fetch): Promise<boolean> {
+		return await deleteTestAPI(f, this._id);
+	}
+
 	static parse(data: any): Test | null {
 		if (data === null) {
 			toastAlert('Failed to parse test data');
@@ -79,7 +94,7 @@ export class TestTyping extends Test {
 	private _text: string;
 	private _duration: number;
 	private _repeat: number;
-	private _explainations: string;
+	private _explanations: string;
 
 	constructor(
 		id: number,
@@ -87,13 +102,13 @@ export class TestTyping extends Test {
 		text: string,
 		duration: number,
 		repeat: number,
-		explainations: string
+		explanations: string
 	) {
 		super(id, title);
 		this._text = text;
 		this._duration = duration;
 		this._repeat = repeat;
-		this._explainations = explainations;
+		this._explanations = explanations;
 	}
 
 	get text(): string {
@@ -108,8 +123,8 @@ export class TestTyping extends Test {
 		return this._repeat;
 	}
 
-	get explainations(): string {
-		return this._explainations;
+	get explanations(): string {
+		return this._explanations;
 	}
 
 	get initialDuration(): number {
@@ -143,7 +158,7 @@ export class TestTyping extends Test {
 			data.test_typing.text,
 			data.test_typing.duration,
 			data.test_typing.repeat,
-			data.test_typing.explainations
+			data.test_typing.explanations
 		);
 	}
 }
diff --git a/frontend/src/routes/admin/studies/+page.svelte b/frontend/src/routes/admin/studies/+page.svelte
index 254ee148ed71f390f69d5e95839a5fc87ae1454c..7d9be1f23f6743d4d11869b72f0258ad483ee90c 100644
--- a/frontend/src/routes/admin/studies/+page.svelte
+++ b/frontend/src/routes/admin/studies/+page.svelte
@@ -52,6 +52,7 @@
 		{/each}
 	</tbody>
 </table>
-<div class="mt-8 w-[64rem] mx-auto">
+<div class="mt-8 mx-auto w-[64rem] flex justify-between">
 	<a class="button" href="/admin/studies/new">{$t('studies.create')}</a>
+	<a class="btn" href="/admin/tests">{$t('tests.manage')}</a>
 </div>
diff --git a/frontend/src/routes/admin/studies/new/+page.server.ts b/frontend/src/routes/admin/studies/new/+page.server.ts
index 09b23ed3b2a4b41fa63736ac19919fb16a9aa842..59965b80532fff6d392d60e2de3092b574892b91 100644
--- a/frontend/src/routes/admin/studies/new/+page.server.ts
+++ b/frontend/src/routes/admin/studies/new/+page.server.ts
@@ -103,4 +103,3 @@ export const actions: Actions = {
 		return redirect(303, '/admin/studies');
 	}
 };
-null as any as Actions;
diff --git a/frontend/src/routes/admin/tests/+page.svelte b/frontend/src/routes/admin/tests/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..7c1f6fa3b9e36545713b6a9880dd6874addc2c0e
--- /dev/null
+++ b/frontend/src/routes/admin/tests/+page.svelte
@@ -0,0 +1,37 @@
+<script lang="ts">
+	import { t } from '$lib/services/i18n';
+	import type { PageData } from './$types';
+	import type { Test } from '$lib/types/tests';
+
+	const { data }: { data: PageData } = $props();
+
+	let tests: Test[] = $state(data.tests);
+</script>
+
+<h1 class="text-xl font-bold m-5 text-center">{$t('header.admin.tests')}</h1>
+
+<table class="table max-w-5xl mx-auto text-left table-fixed">
+	<thead>
+		<tr>
+			<th class="w-24">#</th>
+			<th class="w-32">{$t('utils.words.type')}</th>
+			<th>{$t('utils.words.title')}</th>
+		</tr>
+	</thead>
+	<tbody>
+		{#each tests as test (test.id)}
+			<tr
+				class="hover:bg-gray-100 hover:cursor-pointer"
+				onclick={() => (window.location.href = `/admin/tests/${test.id}`)}
+			>
+				<td>{test.id}</td>
+				<td>{test.type}</td>
+				<td>{test.title}</td>
+			</tr>
+		{/each}
+	</tbody>
+</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>
+</div>
diff --git a/frontend/src/routes/admin/tests/+page.ts b/frontend/src/routes/admin/tests/+page.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c8643713a2b0ea63f92abbd4135a63bd0866fe33
--- /dev/null
+++ b/frontend/src/routes/admin/tests/+page.ts
@@ -0,0 +1,11 @@
+import { getSurveysAPI } from '$lib/api/survey';
+import { type Load } from '@sveltejs/kit';
+import { Test } from '$lib/types/tests';
+
+export const load: Load = async ({ fetch }) => {
+	const tests = Test.parseAll(await getSurveysAPI(fetch));
+
+	return {
+		tests
+	};
+};
diff --git a/frontend/src/routes/admin/tests/[id]/+page.server.ts b/frontend/src/routes/admin/tests/[id]/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5ecbb67f23bc3139a80d8d2a2cfb3eede42a23a7
--- /dev/null
+++ b/frontend/src/routes/admin/tests/[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/[id]/+page.svelte b/frontend/src/routes/admin/tests/[id]/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..d3e6ef7c67755d69e05d3e82fc5cdbb2a12b649c
--- /dev/null
+++ b/frontend/src/routes/admin/tests/[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 } = data;
+</script>
+
+<TestForm {test} />
diff --git a/frontend/src/routes/admin/tests/[id]/+page.ts b/frontend/src/routes/admin/tests/[id]/+page.ts
new file mode 100644
index 0000000000000000000000000000000000000000..71076a850d6299a3784de794ffdd803c240baa2c
--- /dev/null
+++ b/frontend/src/routes/admin/tests/[id]/+page.ts
@@ -0,0 +1,15 @@
+import { Test } from '$lib/types/tests';
+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 (await fetch(`/api/tests/${id}`)).json();
+	const test = Test.parse(testRaw);
+
+	return { test };
+};
diff --git a/frontend/src/routes/admin/tests/new/+page.server.ts b/frontend/src/routes/admin/tests/new/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fd629c3d9571a485ee5e974abcef3f6fe785dded
--- /dev/null
+++ b/frontend/src/routes/admin/tests/new/+page.server.ts
@@ -0,0 +1,49 @@
+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 type = formData.get('type')?.toString();
+
+		if (!title || !type || (type !== 'typing' && type !== 'task')) {
+			return {
+				message: 'Invalid request: Missing required fields'
+			};
+		}
+
+		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 id = await createTestTypingAPI(fetch, title, explanation, text, repeat, duration);
+
+			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
new file mode 100644
index 0000000000000000000000000000000000000000..e62bf17d7b045c4057ff603f653be1ccd15be49f
--- /dev/null
+++ b/frontend/src/routes/admin/tests/new/+page.svelte
@@ -0,0 +1,5 @@
+<script lang="ts">
+	import TestForm from '$lib/components/tests/TestForm.svelte';
+</script>
+
+<TestForm test={null} />