gellify logoGellify dev

How to unit and integration test

Table schema definition guidelines

Questa pagina descrive come sono configurati e strutturati i test nel progetto. Il setup copre test unitari, test di integrazione, isolamento del database in memoria e configurazione della coverage.

Configurations

Script di test

I test vengono eseguiti tramite Bun, con un file di setup caricato prima dei test:

"scripts": {
  "test": "bun test --preload ./src/setup.ts"
}

Configurazione bunfig.toml

La configurazione per il test è definita in bunfig.toml. Alcuni parametri chiave:

[test]
coverage = false                       # Abilita la coverage
coverageReporter = ["lcov"]            # Report in formato LCOV
coverageDir = "coverage"               # Directory di output per i report
coverageThreshold = 0.9                # Copertura minima richiesta (linee e funzioni)

# Percorsi esclusi dalla coverage
coveragePathIgnorePatterns = [
  "src/app/**",
  "src/components/**",
  "src/hooks/**",
  "src/instrumentation.ts",
  "src/middleware.ts",
  "*.config.js",
  "*.config.ts",
  "*.config.cjs",
]

✅ Puoi modificare coverageThreshold per valori personalizzati su linee, funzioni o statements.

Test Environment Setup

I test (soprattutto quelli di integrazione) utilizzano un database SQLite in memoria tramite PGlite, garantendo isolamento e velocità.

import { PGlite } from "@electric-sql/pglite";
import { afterAll, afterEach, beforeEach, mock } from "bun:test";
import { drizzle } from "drizzle-orm/pglite";
import { migrate } from "drizzle-orm/pglite/migrator";
import { reset } from "drizzle-seed";

import { db } from "./server/db";
import { schema } from "./server/db/schema";

let client: PGlite;

await mock.module("./server/db", () => {
  client = new PGlite(); // DB in memoria
  const db = drizzle({
    client,
    schema,
    logger: false,
    casing: "snake_case",
  });
  return { client, db };
});

beforeEach(async () => {
  await migrate(db, { migrationsFolder: "src/server/db/migrations" });
});

afterEach(async () => {
  await reset(db, schema); // Pulisce tutte le tabelle tra i test
});

afterAll(async () => {
  await client.close(); // Chiude la connessione al DB
});

Laddove un PGLite in memory non fosse sufficiente è possibile integrare facilmente @testcontainer/postgres per simulare un DB 1:1 tramite Docker

import { PostgreSqlContainer } from "@testcontainers/postgresql";
import { afterAll, afterEach, beforeEach, mock } from "bun:test";
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/pglite/migrator";
import { reset } from "drizzle-seed";
import postgres from "postgres";

import type { StartedPostgreSqlContainer } from "@testcontainers/postgresql";
import { db } from "./server/db";
import { schema } from "./server/db/schema";

let container: StartedPostgreSqlContainer;

await mock.module("./server/db", () => {
  container = await new PostgreSqlContainer("postgres:17")
  const connectionString = container.getConnectionUri();
  const client = postgres(connectionString);
  const db = drizzle(client, { logger: false, schema });
  return { client, db };
});

beforeEach(async () => {
  await migrate(db, { migrationsFolder: "src/server/db/migrations" });
});

afterEach(async () => {
  await reset(db, schema); // Pulisce tutte le tabelle tra i test
});

afterAll(async () => {
  await container.stop(); // Spegne il container
});

Unit Tests

I test unitari verificano il comportamento di funzioni pure e logica isolata, senza accedere a risorse esterne (DB, file system, ecc.).

Esempio: testare una funzione che mescola todo

describe("shuffleTodos", () => {
  it("returns a new array with the same items", () => {
    // ...
  });

  it("shuffles the array (order may change)", () => {
    // ...
  });

  it("handles empty array", () => {
    // ...
  });

  it("handles single item array", () => {
    // ...
  });
});

✳️ In questo esempio la funzione shuffleTodos è testata in isolamento, usando array mockati.

Integration Tests

I test di integrazione verificano il comportamento dell’applicazione nella sua interezza, incluso accesso al database, logica dei service, e interazioni tra funzioni.

Esempio: CRUD dei Todo

test("create user", async () => {
  const todo = await createTodo({ text: "text", completed: false });
  expect(todo).toBeDefined();
});

Altri scenari coperti:

  • lettura di liste vuote o popolate
  • aggiornamento e soft delete dei record
  • test dell’effetto combinato tra più funzioni

ℹ️ Ogni test è eseguito su un database pulito, migrato e resettato automaticamente tramite il setup definito.

Coverage

Anche se disabilitata di default (coverage = false), la coverage può essere abilitata facilmente per monitorare la qualità dei test:

bun test --coverage

  • I file ignorati sono definiti in coveragePathIgnorePatterns
  • Il report completo è generato nella cartella coverage/
  • Il formato lcov può essere utilizzato per tool come Codecov o Coveralls

📈 Imposta coverageThreshold per evitare merge con bassa copertura.

Best Practices

  • ✅ Scrivi test unitari per logica pura, senza dipendenze
  • ✅ Usa test di integrazione per service, DB e logica end-to-end
  • ✅ Mantieni i test veloci, isolati e deterministici
  • ✅ Usa bun --coverage localmente o in CI per monitorare la qualità
  • ❌ Non testare implementazioni, testa comportamenti

Conclusione

Questa configurazione ti permette di scrivere test efficaci, scalabili e con un feedback rapido. L’approccio misto (unit + integration) garantisce una copertura completa della logica di business e dell’infrastruttura.

Per domande o problemi con l’ambiente di test, consulta il file src/setup.ts o la documentazione dei pacchetti usati (bun, drizzle).

Edit on GitHub