Introduzione
I moduli JavaScript non servono soltanto a organizzare il codice: rendono anche più semplice testare ogni parte dell’applicazione in modo indipendente. Grazie a export possiamo esporre solo le funzionalità pubbliche di un modulo, mentre con import possiamo riutilizzarle nei test o in altri file.
Un problema frequente riguarda le dipendenze esterne. Immaginiamo una funzione che calcola il totale di un ordine, ma che legge l’IVA da un servizio remoto. Testarla direttamente sarebbe lento e poco affidabile, perché il risultato dipenderebbe dalla rete. Una soluzione pratica consiste nel separare:
- la logica pura, facile da verificare;
- l’accesso alle dipendenze esterne;
- il modulo principale, che collega le due parti.
In questo tutorial useremo i moduli ES moderni e il test runner integrato in Node.js, chiamato node:test. L’obiettivo è creare un piccolo calcolatore di prezzi che utilizza una funzione esterna per ottenere l’aliquota IVA.
Codice completo
Organizziamo il progetto in questo modo:
negozio/
├── package.json
├── iva.js
├── ordine.js
└── ordine.test.js Per indicare a Node.js che vogliamo usare i moduli ES, inseriamo "type": "module" nel file package.json:
{
"name": "esempio-moduli-test",
"version": "1.0.0",
"type": "module",
"scripts": {
"test": "node --test"
}
} Il modulo iva.js rappresenta una dipendenza separata:
// iva.js
// In un´applicazione reale questo valore potrebbe arrivare
// da un database o da un servizio remoto.
export async function recuperaAliquotaIVA() {
return 0.22;
} Ora creiamo ordine.js. La funzione calcolaTotale riceve la funzione che recupera l’IVA come parametro:
// ordine.js
/**
* Calcola il prezzo finale di un prodotto.
*
* @param {number} prezzo
* @param {number} quantita
* @param {Function} recuperaIVA
* @returns {Promise<number>}
*/
export async function calcolaTotale(prezzo, quantita, recuperaIVA) {
if (prezzo < 0) {
throw new Error("Il prezzo non può essere negativo");
}
if (!Number.isInteger(quantita) || quantita <= 0) {
throw new Error("La quantità deve essere un intero positivo");
}
const aliquota = await recuperaIVA();
const imponibile = prezzo * quantita;
const totale = imponibile * (1 + aliquota);
return Number(totale.toFixed(2));
} Il file di test importa la funzione da verificare, ma non usa la vera dipendenza. Al suo posto definiamo una funzione finta, chiamata spesso fake o stub:
// ordine.test.js
import test from "node:test";
import assert from "node:assert/strict";
import { calcolaTotale } from "./ordine.js";
test("calcola il totale applicando l´IVA", async () => {
// Dipendenza controllata: restituisce sempre il 22%
const ivaFinta = async () => 0.22;
const totale = await calcolaTotale(10, 2, ivaFinta);
assert.equal(totale, 24.4);
});
test("gestisce un´aliquota IVA diversa", async () => {
const ivaFinta = async () => 0.10;
const totale = await calcolaTotale(50, 1, ivaFinta);
assert.equal(totale, 55);
});
test("rifiuta un prezzo negativo", async () => {
const ivaFinta = async () => 0.22;
await assert.rejects(
calcolaTotale(-5, 1, ivaFinta),
{
message: "Il prezzo non può essere negativo"
}
);
});
test("rifiuta una quantità non valida", async () => {
const ivaFinta = async () => 0.22;
await assert.rejects(
calcolaTotale(10, 0, ivaFinta),
{
message: "La quantità deve essere un intero positivo"
}
);
}); Per eseguire i test usiamo:
npm test Spiegazione
Il punto centrale dell’esempio è l’iniezione della dipendenza. Invece di importare direttamente recuperaAliquotaIVA dentro ordine.js, la passiamo alla funzione:
calcolaTotale(10, 2, ivaFinta) In questo modo calcolaTotale non conosce la provenienza del dato. Può usare una funzione che interroga un’API, una funzione che legge un file oppure una funzione finta creata durante il test.
Questa scelta evita test fragili. Se il servizio remoto è temporaneamente indisponibile, i test della logica di calcolo continuano a funzionare. Inoltre, possiamo simulare facilmente casi difficili da riprodurre nella realtà, come un’aliquota pari a zero o un errore di rete.
Gli elementi export e import definiscono un contratto chiaro:
ordine.jsesporta la funzione pubblicacalcolaTotale;ordine.test.jsimporta soltanto ciò che deve verificare;- la funzione interna non dipende da variabili globali nascoste;
- la dipendenza viene sostituita senza modificare il modulo originale.
In un’applicazione reale, il codice di produzione potrebbe collegare i due moduli in questo modo:
// applicazione.js
import { recuperaAliquotaIVA } from "./iva.js";
import { calcolaTotale } from "./ordine.js";
const totale = await calcolaTotale(
25,
3,
recuperaAliquotaIVA
);
console.log(`Totale ordine: ${totale} euro`); Best practice
- Esporta solo l’API necessaria: mantieni private le funzioni di supporto che non devono essere usate dall’esterno.
- Preferisci funzioni pure: una funzione che riceve i dati tramite parametri è più semplice da testare rispetto a una funzione che legge direttamente variabili globali.
- Inietta le dipendenze: passa client HTTP, lettori di file o funzioni di configurazione come argomenti quando è ragionevole farlo.
- Usa test deterministici: evita date, numeri casuali e richieste di rete reali nei test della logica.
- Testa anche gli errori: un modulo affidabile deve definire chiaramente come reagisce a input non validi.
- Mantieni i percorsi completi: nei moduli ES di Node.js importa i file con l’estensione, ad esempio
./ordine.js. - Non modificare gli export importati: gli import ES sono associazioni in sola lettura; se serve cambiare lo stato, esponi funzioni dedicate.
Riepilogo
Import ed export permettono di costruire moduli indipendenti e facilmente verificabili. Separando la logica principale dalle dipendenze esterne, possiamo sostituire queste ultime con implementazioni finte durante i test.
Il risultato è un codice più prevedibile, veloce da testare e meno legato a database, API o sistema operativo. La tecnica mostrata, chiamata iniezione delle dipendenze, è particolarmente utile nei progetti medi e grandi, dove la separazione delle responsabilità diventa essenziale.
