test: add mock DB helper for unit tests
Check Deno code / Check Deno code (pull_request) Failing after 39s
Check Deno code / Check Deno code (pull_request) Failing after 39s
test: add tests for fixtures, mock fetch, mock db, and happy-dom - Add comprehensive fixture shape tests. - Expand mockFetch to support methods, status codes, and body tracking. - Introduce getFetchCalls to inspect intercepted requests. - Add mockDb helper for in-memory DB operations. - Reorganize tests for clarity and coverage. - Ensure happy-dom setup/cleanup works correctly.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
// Mock de la couche Drizzle pour les tests unitaires/intégration
|
||||
// Permet de tester les handlers sans connexion PostgreSQL
|
||||
|
||||
export interface MockQueryResult<T> {
|
||||
rows: T[];
|
||||
}
|
||||
|
||||
export interface MockDbConfig {
|
||||
// Table name → array of rows
|
||||
// deno-lint-ignore no-explicit-any
|
||||
tables: Record<string, Record<string, any>[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un mock de la DB Drizzle.
|
||||
* Simule select/insert/update/delete avec un store en mémoire.
|
||||
*
|
||||
* Usage :
|
||||
* ```ts
|
||||
* const db = createMockDb({
|
||||
* tables: {
|
||||
* students: [{ numEtud: 21212006, nom: "Dupont", ... }],
|
||||
* notes: [],
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Lire toutes les lignes d'une table
|
||||
* const rows = db.getTable("students");
|
||||
*
|
||||
* // Insérer
|
||||
* db.insert("students", { numEtud: 21212009, nom: "Test", ... });
|
||||
*
|
||||
* // Trouver par clé
|
||||
* const student = db.findOne("students", (r) => r.numEtud === 21212006);
|
||||
*
|
||||
* // Supprimer
|
||||
* db.deleteWhere("students", (r) => r.numEtud === 21212006);
|
||||
* ```
|
||||
*/
|
||||
export function createMockDb(config: MockDbConfig) {
|
||||
// Deep clone pour éviter les mutations entre tests
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const tables: Record<string, Record<string, any>[]> = {};
|
||||
for (const [name, rows] of Object.entries(config.tables)) {
|
||||
tables[name] = rows.map((r) => ({ ...r }));
|
||||
}
|
||||
|
||||
return {
|
||||
/** Retourne toutes les lignes d'une table */
|
||||
getTable<T = Record<string, unknown>>(name: string): T[] {
|
||||
return (tables[name] ?? []) as T[];
|
||||
},
|
||||
|
||||
/** Retourne les lignes qui matchent le filtre */
|
||||
findMany<T = Record<string, unknown>>(
|
||||
name: string,
|
||||
predicate: (row: T) => boolean,
|
||||
): T[] {
|
||||
return (this.getTable<T>(name)).filter(predicate);
|
||||
},
|
||||
|
||||
/** Retourne la première ligne qui matche, ou undefined */
|
||||
findOne<T = Record<string, unknown>>(
|
||||
name: string,
|
||||
predicate: (row: T) => boolean,
|
||||
): T | undefined {
|
||||
return (this.getTable<T>(name)).find(predicate);
|
||||
},
|
||||
|
||||
/** Insère une ligne dans la table */
|
||||
insert<T = Record<string, unknown>>(name: string, row: T): T {
|
||||
if (!tables[name]) tables[name] = [];
|
||||
const copy = { ...row } as T;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
tables[name].push(copy as any);
|
||||
return copy;
|
||||
},
|
||||
|
||||
/** Met à jour les lignes qui matchent le prédicat */
|
||||
updateWhere<T = Record<string, unknown>>(
|
||||
name: string,
|
||||
predicate: (row: T) => boolean,
|
||||
updates: Partial<T>,
|
||||
): number {
|
||||
const rows = this.getTable<T>(name);
|
||||
let count = 0;
|
||||
for (const row of rows) {
|
||||
if (predicate(row)) {
|
||||
Object.assign(row as Record<string, unknown>, updates);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
},
|
||||
|
||||
/** Supprime les lignes qui matchent le prédicat */
|
||||
deleteWhere<T = Record<string, unknown>>(
|
||||
name: string,
|
||||
predicate: (row: T) => boolean,
|
||||
): number {
|
||||
const before = (tables[name] ?? []).length;
|
||||
tables[name] = (tables[name] ?? []).filter(
|
||||
(r) => !predicate(r as unknown as T),
|
||||
);
|
||||
return before - tables[name].length;
|
||||
},
|
||||
|
||||
/** Vide une table */
|
||||
clear(name: string): void {
|
||||
tables[name] = [];
|
||||
},
|
||||
|
||||
/** Vide toutes les tables */
|
||||
reset(): void {
|
||||
for (const name of Object.keys(tables)) {
|
||||
tables[name] = [];
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type MockDb = ReturnType<typeof createMockDb>;
|
||||
+229
-24
@@ -1,24 +1,46 @@
|
||||
import { assertEquals, assertExists } from "@std/assert";
|
||||
import { mockFetch, restoreFetch } from "../helpers/api_mock.ts";
|
||||
import { notes, students } from "../helpers/fixtures.ts";
|
||||
import {
|
||||
getFetchCalls,
|
||||
mockFetch,
|
||||
restoreFetch,
|
||||
} from "../helpers/api_mock.ts";
|
||||
import { createMockDb } from "../helpers/db_mock.ts";
|
||||
import {
|
||||
type Student,
|
||||
ERROR_CONFLICT,
|
||||
ERROR_NOT_FOUND,
|
||||
modules,
|
||||
notes,
|
||||
students,
|
||||
} from "../helpers/fixtures.ts";
|
||||
import { cleanupDOM, setupDOM } from "../helpers/render.ts";
|
||||
|
||||
Deno.test("fixtures - students have expected shape", () => {
|
||||
// --- Fixtures ---
|
||||
|
||||
Deno.test("fixtures - students match API shape", () => {
|
||||
assertEquals(students.length, 3);
|
||||
assertEquals(students[0].nom, "Dupont");
|
||||
assertExists(students[0].numEtud);
|
||||
assertEquals(students[0].numEtud, 21212006);
|
||||
assertEquals(students[0].idPromo, "4AFISE25/26");
|
||||
assertEquals(typeof students[0].idPromo, "string");
|
||||
});
|
||||
|
||||
Deno.test("mockFetch - returns mocked data for matching route", async () => {
|
||||
mockFetch({
|
||||
"/students": students,
|
||||
"/notes": notes,
|
||||
});
|
||||
Deno.test("fixtures - modules have string ids", () => {
|
||||
assertEquals(modules[0].id, "JIN702C");
|
||||
assertEquals(typeof modules[0].id, "string");
|
||||
});
|
||||
|
||||
Deno.test("fixtures - notes use decimal values", () => {
|
||||
assertEquals(notes[0].note, 15.5);
|
||||
assertEquals(notes[0].idModule, "JIN702C");
|
||||
});
|
||||
|
||||
// --- Mock fetch simple (GET 200) ---
|
||||
|
||||
Deno.test("mockFetch - GET returns mocked data", async () => {
|
||||
mockFetch({ "/students": students });
|
||||
try {
|
||||
const res = await fetch("http://localhost/api/students");
|
||||
assertEquals(res.status, 200);
|
||||
|
||||
const data = await res.json();
|
||||
assertEquals(data.length, 3);
|
||||
assertEquals(data[0].nom, "Dupont");
|
||||
@@ -29,7 +51,6 @@ Deno.test("mockFetch - returns mocked data for matching route", async () => {
|
||||
|
||||
Deno.test("mockFetch - returns 404 for unknown routes", async () => {
|
||||
mockFetch({});
|
||||
|
||||
try {
|
||||
const res = await fetch("http://localhost/api/unknown");
|
||||
assertEquals(res.status, 404);
|
||||
@@ -38,24 +59,208 @@ Deno.test("mockFetch - returns 404 for unknown routes", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Mock fetch avancé (méthodes + status codes) ---
|
||||
|
||||
Deno.test("mockFetch - POST 201 created", async () => {
|
||||
const newStudent = students[0];
|
||||
mockFetch({
|
||||
"/students": { method: "POST", status: 201, body: newStudent },
|
||||
});
|
||||
try {
|
||||
const res = await fetch("http://localhost/api/students", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(newStudent),
|
||||
});
|
||||
assertEquals(res.status, 201);
|
||||
const data = await res.json();
|
||||
assertEquals(data.numEtud, 21212006);
|
||||
} finally {
|
||||
restoreFetch();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("mockFetch - DELETE 204 no content", async () => {
|
||||
mockFetch({
|
||||
"/students/21212006": { method: "DELETE", status: 204 },
|
||||
});
|
||||
try {
|
||||
const res = await fetch("http://localhost/api/students/21212006", {
|
||||
method: "DELETE",
|
||||
});
|
||||
assertEquals(res.status, 204);
|
||||
assertEquals(res.body, null);
|
||||
} finally {
|
||||
restoreFetch();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("mockFetch - 404 error response", async () => {
|
||||
mockFetch({
|
||||
"/students/99999": { status: 404, body: ERROR_NOT_FOUND },
|
||||
});
|
||||
try {
|
||||
const res = await fetch("http://localhost/api/students/99999");
|
||||
assertEquals(res.status, 404);
|
||||
const data = await res.json();
|
||||
assertEquals(data.error, "Ressource introuvable");
|
||||
} finally {
|
||||
restoreFetch();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("mockFetch - 409 conflict", async () => {
|
||||
mockFetch({
|
||||
"/enseignements": { method: "POST", status: 409, body: ERROR_CONFLICT },
|
||||
});
|
||||
try {
|
||||
const res = await fetch("http://localhost/api/enseignements", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ idProf: 1, idModule: "JIN702C", idPromo: "4AFISE25/26" }),
|
||||
});
|
||||
assertEquals(res.status, 409);
|
||||
} finally {
|
||||
restoreFetch();
|
||||
}
|
||||
});
|
||||
|
||||
// --- getFetchCalls ---
|
||||
|
||||
Deno.test("getFetchCalls - tracks all intercepted calls", async () => {
|
||||
mockFetch({ "/notes": notes });
|
||||
try {
|
||||
await fetch("http://localhost/api/notes");
|
||||
await fetch("http://localhost/api/notes?numEtud=21212006");
|
||||
const calls = getFetchCalls();
|
||||
assertEquals(calls.length, 2);
|
||||
assertEquals(calls[0].method, "GET");
|
||||
assertEquals(calls[1].url, "http://localhost/api/notes?numEtud=21212006");
|
||||
} finally {
|
||||
restoreFetch();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("getFetchCalls - captures POST body", async () => {
|
||||
mockFetch({ "/notes": { method: "POST", status: 201, body: notes[0] } });
|
||||
try {
|
||||
await fetch("http://localhost/api/notes", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(notes[0]),
|
||||
});
|
||||
const calls = getFetchCalls();
|
||||
assertEquals(calls.length, 1);
|
||||
assertEquals(calls[0].method, "POST");
|
||||
assertEquals((calls[0].body as { note: number }).note, 15.5);
|
||||
} finally {
|
||||
restoreFetch();
|
||||
}
|
||||
});
|
||||
|
||||
// --- Mock DB ---
|
||||
|
||||
Deno.test("mockDb - getTable returns seeded rows", () => {
|
||||
const db = createMockDb({ tables: { students: [...students] } });
|
||||
assertEquals(db.getTable("students").length, 3);
|
||||
});
|
||||
|
||||
Deno.test("mockDb - findOne by key", () => {
|
||||
const db = createMockDb({ tables: { students: [...students] } });
|
||||
const found = db.findOne<Student>("students", (r) => r.numEtud === 21212006);
|
||||
assertExists(found);
|
||||
assertEquals(found.nom, "Dupont");
|
||||
});
|
||||
|
||||
Deno.test("mockDb - findOne returns undefined for missing", () => {
|
||||
const db = createMockDb({ tables: { students: [...students] } });
|
||||
const found = db.findOne<Student>("students", (r) => r.numEtud === 99999);
|
||||
assertEquals(found, undefined);
|
||||
});
|
||||
|
||||
Deno.test("mockDb - insert adds a row", () => {
|
||||
const db = createMockDb({ tables: { students: [] } });
|
||||
const newStudent: Student = {
|
||||
numEtud: 21212099,
|
||||
nom: "Test",
|
||||
prenom: "User",
|
||||
idPromo: "4AFISE25/26",
|
||||
};
|
||||
db.insert("students", newStudent);
|
||||
assertEquals(db.getTable("students").length, 1);
|
||||
assertEquals(
|
||||
db.findOne<Student>("students", (r) => r.numEtud === 21212099)?.nom,
|
||||
"Test",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("mockDb - updateWhere modifies matching rows", () => {
|
||||
const db = createMockDb({ tables: { students: [...students] } });
|
||||
const updated = db.updateWhere<Student>(
|
||||
"students",
|
||||
(r) => r.numEtud === 21212006,
|
||||
{ prenom: "Marie" },
|
||||
);
|
||||
assertEquals(updated, 1);
|
||||
assertEquals(
|
||||
db.findOne<Student>("students", (r) => r.numEtud === 21212006)?.prenom,
|
||||
"Marie",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("mockDb - deleteWhere removes matching rows", () => {
|
||||
const db = createMockDb({ tables: { students: [...students] } });
|
||||
const deleted = db.deleteWhere<Student>(
|
||||
"students",
|
||||
(r) => r.numEtud === 21212006,
|
||||
);
|
||||
assertEquals(deleted, 1);
|
||||
assertEquals(db.getTable("students").length, 2);
|
||||
});
|
||||
|
||||
Deno.test("mockDb - findMany with filter", () => {
|
||||
const db = createMockDb({ tables: { students: [...students] } });
|
||||
const promo4 = db.findMany<Student>(
|
||||
"students",
|
||||
(r) => r.idPromo === "4AFISE25/26",
|
||||
);
|
||||
assertEquals(promo4.length, 2);
|
||||
});
|
||||
|
||||
Deno.test("mockDb - reset clears all tables", () => {
|
||||
const db = createMockDb({
|
||||
tables: { students: [...students], notes: [...notes] },
|
||||
});
|
||||
db.reset();
|
||||
assertEquals(db.getTable("students").length, 0);
|
||||
assertEquals(db.getTable("notes").length, 0);
|
||||
});
|
||||
|
||||
Deno.test("mockDb - isolated between instances", () => {
|
||||
const db1 = createMockDb({ tables: { students: [...students] } });
|
||||
const db2 = createMockDb({ tables: { students: [...students] } });
|
||||
db1.deleteWhere<Student>("students", () => true);
|
||||
assertEquals(db1.getTable("students").length, 0);
|
||||
assertEquals(db2.getTable("students").length, 3);
|
||||
});
|
||||
|
||||
// --- happy-dom ---
|
||||
|
||||
Deno.test({
|
||||
name: "happy-dom - document is available after setup",
|
||||
sanitizeResources: false,
|
||||
sanitizeOps: false,
|
||||
fn() {
|
||||
setupDOM();
|
||||
setupDOM();
|
||||
try {
|
||||
const doc = globalThis.document;
|
||||
assertExists(doc);
|
||||
|
||||
try {
|
||||
const doc = globalThis.document;
|
||||
assertExists(doc);
|
||||
const div = doc.createElement("div");
|
||||
div.textContent = "hello";
|
||||
doc.body.appendChild(div);
|
||||
|
||||
const div = doc.createElement("div");
|
||||
div.textContent = "hello";
|
||||
doc.body.appendChild(div);
|
||||
|
||||
assertEquals(doc.body.textContent, "hello");
|
||||
} finally {
|
||||
cleanupDOM();
|
||||
}
|
||||
assertEquals(doc.body.textContent, "hello");
|
||||
} finally {
|
||||
cleanupDOM();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user