Probabilmente ti trovi in una di queste due situazioni: o il tuo progetto JavaScript ha quasi zero test e ogni refactor sembra rischioso, o hai già dei test ma metà sono lenti, instabili e difficili da fidarsi.
La situazione peggiora con le Capacitor e Electron app. Una semplice feature può toccare logica di business condivisa, API del browser, plugin nativi, file locali, IPC e servizi remoti nello stesso flusso. Se testi questi componenti nel modo sbagliato, la tua suite diventerà un labirinto di dipendenze fake. Se li testi nel modo giusto, otterrai un feedback rapido sulla logica che si rompe.
I buoni unit test in JavaScript non iniziano con una sintassi di matcher ingegnosa. Iniziano con un confine disciplinato: testa la logica pura direttamente, isola gli effetti collaterali ed evita di scrivere test che si rompono non appena rinomini una funzione interna.
Indice dei contenuti
- Scegliere il framework di testing per JavaScript
- Setup del progetto e primo test
- Padroneggiare Mock e Code asincroni
- Strategie avanzate per test robusti
- Test per CI, Capacitor e app Electron
- Domande frequenti sui test unitari in JavaScript
Scegliere il proprio framework di testing JavaScript
Un progetto JavaScript professionale ha bisogno di un vero test runner. Script ad hoc e controlli manuali in console non reggono quando più sviluppatori lavorano sullo stesso codebase. Servono test discovery, assertion, gestione dell'async, mock e un modo per eseguire tutto in modo coerente in locale e in CI.
Le linee guida attuali convergono su un piccolo insieme di opzioni mainstream. Jest, Mocha e Jasmine vengono indicati ripetutamente come i framework principali, con Jest spesso citato per la struttura dei test integrata, le assertion, il mocking e il supporto async in un unico pacchetto, come mostrato in questo lab di testing JavaScript di Pluralsight.

Perché un framework non è opzionale
Il primo errore che commettono i team è trattare gli unit test come un'attività secondaria. Questo porta spesso a nomi di file incoerenti, assertion personalizzate che nessuno ricorda e helper comprensibili da una sola persona.
Un framework ti offre un linguaggio condiviso:
- Struttura dei test con
describeetestoit - Assertion con matcher leggibili
- Hooks per setup e teardown
- Supporto async per promise e timer
- Strumenti di mocking per dipendenze esterne
Se il tuo team necessita di una visione più ampia dell'automazione dei test oltre il livello unitario, Capgo offre una panoramica utile su test automatizzati nei workflow di rilascio app.
Jest vs Mocha a confronto
Jest e Mocha rappresentano due filosofie diverse.
Jest è l'opzione all-in-one. Include quasi tutto ciò che serve ai team fin dal primo giorno.
Mocha è più modulare. Ti fornisce un runner e ti lascia il compito di assemblare il resto dello stack.
| Funzionalità | Jest | Mocha |
|---|---|---|
| Complessità setup | Minore per la maggior parte dei team | Maggiore, poiché si aggiungono librerie di assertion e mocking |
| Assertion | Integrato | Solitamente accoppiato a un'altra libreria |
| Mocking | Integrato | Solitamente accoppiato a un'altra libreria |
| Test asincroni | Integrato e immediato | Supportato, ma dipende dal setup circostante |
| Workflow di coverage | Solitamente integrato nella stessa toolchain | Spesso composto da più strumenti |
| Miglior scelta | Nuovi progetti, team che cercano coerenza | Stack legacy, team che preferiscono il controllo modulare |
Regola pratica: Se il tuo team deve scegliere quale assertion e mocking library abbinare al runner, probabilmente ti serve Jest.
Cosa consiglio alla maggior parte dei team
Per la maggior parte dei progetti moderni, sceglierei Jest a meno che il codebase non abbia motivi validi per restare su Mocha. Questa raccomandazione diventa ancora più forte se l'applicazione include Capacitor o Electron, perché quei progetti hanno già troppi elementi in movimento. Ridurre la frammentazione dei tool di testing ripaga velocemente.
Mocha ha ancora senso in servizi Node.js datati o codebase storiche dove l'ecosistema è già consolidato. Ma per un ingegnere mid-level che deve configurare una suite robusta da zero, Jest solitamente riduce l'attrito invece di aumentarlo.
Una nota importante sull'ambito. Cypress e Playwright sono tool eccellenti, ma risolvono un problema diverso. Sono migliori per test a livello di browser ed end-to-end, non per il ciclo rapido di sviluppo dove dovrebbero risiedere i unit test JavaScript.
Setup del progetto e il tuo primo test
Un setup di testing pulito dovrebbe essere noioso. Se aggiungere il primo test sembra complicato, la suite probabilmente non rimarrà in salute.

Un semplice setup di Jest
Inizia con un progetto JavaScript che abbia già un package.json. Poi aggiungi Jest come dev dependency e configura uno script di test.
{
"scripts": {
"test": "jest"
}
}
Per molti progetti è sufficiente. Potrai aggiungere altra configurazione in seguito se il tuo sistema di moduli, la transpilation o la struttura monorepo lo richiederanno.
Se stai sviluppando un'app Capacitor localmente e vuoi sistemare l'ambiente di dev prima di aggiungere test alla logica condivisa, la guida di Capgo per configurare un ambiente locale Capacitor è un ottimo punto di riferimento.
Scrivi il test prima del code
Il pattern test-first non è solo una preferenza personale. Le linee guida JavaScript del Consumer Financial Protection Bureau statunitense raccomandano esplicitamente di scrivere prima il test, organizzare i test con describe e it, e strutturare i controlli tramite expect(...) assertion nelle sue Guida ai unit test in JavaScript.
È fondamentale perché l'approccio test-first cambia il modo in cui progetti code. Le funzioni tendono a diventare più piccole, le dipendenze più visibili e gli effetti collaterali smettono di sporcare la logica che dovrebbe rimanere pura.
Ecco un esempio minimo:
// math.js
function addTax(amount, rate) {
return amount + amount * rate;
}
module.exports = { addTax };
// math.test.js
const { addTax } = require('./math');
describe('addTax', () => {
it('returns the amount with the tax applied', () => {
expect(addTax(100, 0.2)).toBe(120);
});
});
Usa sempre Arrange, Act, Assert
L'oggetto Arrange, Act, Assert mantiene i test leggibili, anche quando diventano più complessi.
- Arrange l'input e tutto il setup necessario.
- Act chiamando la funzione.
- Assert sull'esito.
Applicato a un helper di validazione:
function isSupportedPlatform(platform) {
return ['ios', 'android', 'web', 'desktop'].includes(platform);
}
describe('isSupportedPlatform', () => {
it('returns true for ios', () => {
// Arrange
const platform = 'ios';
// Act
const result = isSupportedPlatform(platform);
// Assert
expect(result).toBe(true);
});
});
I test piccoli invecchiano bene. Un test dovrebbe solitamente rispondere a una sola domanda, non narrare un intero workflow.
Per i progetti Capacitor ed Electron, questa disciplina è ancora più importante perché la tua logica pura spesso risiede accanto a code native o desktop. Mantieni la business rule testabile senza il runtime della piattaforma, e il tuo primo test non sarà l'ultimo utile.
Padroneggiare Mock e Code asincroni
La maggior parte dei bug nelle code applicative non deriva dalla somma di due numeri. Derivano da code che interagiscono con l'esterno: richieste di rete, file, plugin API, timer, canali IPC o layer di storage.
È qui che il mocking aiuta. Ti dà il controllo sui confini, permettendo al test di concentrarsi sul processo decisionale della tua code.

Mocka i confini, non tutto
Le linee guida per test manutenibili enfatizzano copertura del singolo comportamento e un'unica assertion forte per test, e avverte inoltre che l'uso eccessivo di mock rende i test fragili e troppo legati ai dettagli di implementazione, come riassunto in questo articolo di TestRail sui unit test manutenibili.
Questo avvertimento è fondamentale in JavaScript. Spesso i team iniziano mockando ogni modulo importato e finiscono per testare se le funzioni chiamano altre funzioni nell'ordine "corretto", invece di testare il comportamento reale.
Target errato per un test basato su molti mock:
- se l'helper A ha chiamato l'helper B
- se il servizio C ha chiamato il serializer D
- se una funzione privata interna è stata eseguita due volte
Obiettivo migliore:
- cosa ha restituito la funzione
- se ha gestito correttamente il fallimento di una dipendenza
- se ha trasformato i dati nel formato previsto
Un pattern migliore per Capacitor ed Electron code
Nelle app mobile e desktop, preferisco un layer di wrapper attorno alle API native o di piattaforma. In questo modo, i unit test mockano il wrapper e non la piattaforma stessa.
Esempio di struttura:
// cameraGateway.js
async function getPhoto(cameraPlugin) {
return cameraPlugin.getPhoto();
}
module.exports = { getPhoto };
// profilePhotoService.js
async function loadProfilePhoto(cameraGateway) {
const photo = await cameraGateway.getPhoto();
return { path: photo.path, ready: true };
}
module.exports = { loadProfilePhoto };
// profilePhotoService.test.js
const { loadProfilePhoto } = require('./profilePhotoService');
test('returns mapped photo data', async () => {
const fakeCameraGateway = {
getPhoto: jest.fn().mockResolvedValue({ path: '/tmp/pic.jpg' })
};
const result = await loadProfilePhoto(fakeCameraGateway);
expect(result).toEqual({ path: '/tmp/pic.jpg', ready: true });
});
Questo pattern funziona anche con Electron. Avvolgi ipcRendererl'accesso ai file o le integrazioni shell dietro un thin adapter. I unit test colpiscono il service layer, non direttamente il runtime.
Per i team che testano la logica di release e i percorsi di aggiornamento in app Capacitor, Capgo offre una guida su come testare gli aggiornamenti OTA di Capacitor con scenari mock.
Una rapida panoramica può aiutare se il tuo team sta ancora normalizzando lo stile dei test async:
Testare i flussi async senza flakiness
Usa async/await nei test quando il code sotto test restituisce una promise. È più chiaro dei pattern basati su callback ed è più facile da debuggare.
async function fetchProfile(api) {
const response = await api.getUser();
return response.name;
}
test('returns the user name from the API response', async () => {
const api = {
getUser: jest.fn().mockResolvedValue({ name: 'Ava' })
};
const result = await fetchProfile(api);
expect(result).toBe('Ava');
});
Testa anche il percorso di errore:
test('throws when the API request fails', async () => {
const api = {
getUser: jest.fn().mockRejectedValue(new Error('network failed'))
};
await expect(fetchProfile(api)).rejects.toThrow('network failed');
});
Testa sia il happy path che l'ugly path. In produzione, l'ugly path è solitamente quello che gli utenti ricordano meglio.
Strategie avanzate per test robusti
Una suite di test diventa utile quando rimane tale anche dopo i cambiamenti a code. È più difficile che scrivere un mucchio di test che passano.

Usa il testing split come budget
Una guida pratica raccomanda una 70/20/10 suddivisione tra test unitari, di integrazione ed end-to-end, con i test unitari che offrono il feedback più rapido e i fallimenti più stabili. La stessa guida suggerisce che una suite unitaria completa dovrebbe idealmente terminare in meno di 10 secondi, e i controlli pre-commit dovrebbero durare meno di 5 secondi, come indicato in questa Guida ai test con OpenReplay.
Lo considero uno strumento di budgeting, non una religione. Se la maggior parte del lavoro va nei test end-to-end, il team aspetterà troppo per il feedback. Se fai solo unit test, ti perderai i veri confini del sistema.
Per un'app Capacitor o Electron, un buon equilibrio è solitamente questo:
- Unit test per la logica dei prezzi, le regole sui permessi, la serializzazione, l'idoneità agli update, i feature flag e le trasformazioni di stato
- Integration test per adapter di storage, wrapper di plugin e contratti IPC
- E2E test per alcuni flussi critici come login, flusso di acquisto, sincronizzazione o prompt di update
La coverage è una torcia, non un obiettivo
I report di coverage sono utili se aiutano a individuare rami non testati in logiche importanti. Diventano dannosi quando i team inseguono le percentuali di coverage fine a se stesse.
Un validatore di login con test accurati sui casi limite offre molto più valore di un file pieno di asserzioni banali. Questo vale soprattutto per code basati su input, come form, parser, logica delle date e controlli dei permessi. Se il tuo team sta alzando l'asticella della qualità sulle UI basate su validazione, questa guida su padroneggiare la validazione dei form frontend è il complemento ideale per una strategia di unit testing.
I test behavior-first sopravvivono ai refactor
Una suite affidabile deve permetterti di refactorare l'interno senza dover riscrivere metà dei test. Il modo più semplice per riuscirci è testare il comportamento osservabile invece dei dettagli di implementazione.
Casi d'uso che funzionano bene:
- Condizioni limite come input vuoti, valori null, tipi non validi e stringhe troppo lunghe
- Risultati di dominio come "ritorna errore per permessi mancanti"
- Transizioni di stato come “segna l'update come pending dopo la validazione dei metadati”
Casi d'uso che spesso degradano:
- ispezionare le chiamate degli helper interni
- verificare la sequenza dei metodi privati
- mockare ogni layer della catena di chiamata
Per i team che costruiscono processi di release disciplinati, l'articolo di Capgo su quality assurance delle app è utile perché collega il testing alla pipeline di release.
Test per CI, Capacitor e app Electron
Un test che gira solo sulla macchina di un developer non è una rete di sicurezza. È solo un'abitudine locale.
La CI trasforma i unit test JavaScript in infrastruttura di team. Ogni push, pull request o release branch può eseguire gli stessi comandi con le stesse aspettative. Questa coerenza è fondamentale per i progetti Capacitor ed Electron, dove l'environment drift causa errori sottili.
Rendi la CI il percorso di esecuzione predefinito
Al minimo, la tua CI dovrebbe installare le dipendenze ed eseguire la suite di unit test a ogni commit. Mantieni il comando identico allo sviluppo locale, quando possibile.
Un workflow base di GitHub Actions può essere semplice come questo:
name: test
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
È sufficiente per intercettare import errati, assertion fallite e assunzioni accidentali sulla piattaforma prima che arrivino nel branch main.
Per i team mobile che usano pipeline automatizzate, Capgo offre una guida pratica su configurare la CI/CD per le app Capacitor.
Testare le interazioni dei plugin Capacitor
Il modo sbagliato di testare Capacitor code è importare i plugin nativi direttamente in ogni servizio. Questo accoppia la tua suite di test al platform bridge.
Il pattern migliore è un'astrazione leggera:
// deviceStorage.js
async function saveFile(filesystem, path, data) {
return filesystem.writeFile({ path, data });
}
module.exports = { saveFile };
// draftService.js
async function persistDraft(storage, draft) {
await storage.save('draft.json', JSON.stringify(draft));
return { saved: true };
}
module.exports = { persistDraft };
// draftService.test.js
const { persistDraft } = require('./draftService');
test('persists a serialized draft', async () => {
const storage = {
save: jest.fn().mockResolvedValue(undefined)
};
const result = await persistDraft(storage, { title: 'Hello' });
expect(result).toEqual({ saved: true });
});
Lo stesso concetto vale per l'accesso alla camera, i prompt biometrici, la registrazione del push-token e lo stato della rete. Mantieni le chiamate ai plugin negli adapter. Testa la logica dell'app tramite interfacce che controlli.
Testare main, renderer e IPC di Electron code
Le app Electron hanno due punti di giunzione importanti: processo main code e processo renderer code. Non trascurarli nei test.
Un setup affidabile solitamente separa:
- Unit test del Renderer per view model, stato, formattazione e logica UI
- Unit test del processo Main per menu, operazioni file e lifecycle dell'app
- Test del contratto IPC per la struttura dei messaggi e le risposte attese
Esempio di wrapper IPC:
// ipcGateway.js
function sendSettings(ipcRenderer, payload) {
ipcRenderer.send('settings:update', payload);
}
module.exports = { sendSettings };
// ipcGateway.test.js
const { sendSettings } = require('./ipcGateway');
test('sends settings update over ipc', () => {
const ipcRenderer = { send: jest.fn() };
sendSettings(ipcRenderer, { theme: 'dark' });
expect(ipcRenderer.send).toHaveBeenCalledWith('settings:update', { theme: 'dark' });
});
Se in seguito dovessi cambiare l'implementazione interna di un helper, questo test rimarrebbe valido perché verifica il comportamento che conta. Questo è lo standard da seguire per code desktop e mobile.
Domande frequenti sui unit test in JavaScript
Qual è la differenza tra unit, integration ed E2E test
Un unit test verifica una piccola porzione di logica in isolamento. Un test di integrazione verifica se alcuni componenti o servizi lavorano correttamente insieme. Un test end-to-end esegue un intero user journey attraverso l'applicazione in esecuzione.
Usa gli unit test per validare velocemente le business rule. Usa gli integration test per i punti di contatto come storage, plugin wrapper e IPC. Usa i test E2E con parsimonia, solo per i workflow critici.
Dovremmo puntare alla copertura completa?
No. La copertura totale può spingere i team verso test di scarso valore.
La coverage è utile quando rivela code rischiosi che nessuno ha testato. Non è utile se gli ingegneri aggiungono assertion superficiali solo per compiacere una dashboard. Se la tua suite è fragile, più coverage non la salverà.
Come aggiungere test a un codebase esistente
Inizia dove avvengono già i cambiamenti. Non bloccare il team annunciando un rewrite totale della strategia di test.
Una sequenza pratica è questa:
- Proteggi prima i code attivi aggiungendo test ai moduli che tocchi durante lo sviluppo di feature o bug fix
- Estrai la logica pura dai file difficili da testare, così le business rule possono essere testate senza il rumore di framework o runtime
- Aggiungi dei wrapper (seam) attorno a plugin nativi, client di rete, chiamate al filesystem ed Electron IPC
- Rifiuta i pattern fragili quando si introducono i mock. I consigli sulle best practice per il testing in JavaScript sono utilissimi in questa fase, poiché mettono in luce il problema dell'over-mocking e la fragilità dei test che ne deriva
L'obiettivo non è la completezza immediata, ma un miglioramento costante nei punti in cui le regressioni costano di più al team.
Se il tuo team rilascia Capacitor o Electron app e necessita di un processo di rilascio più pulito per le modifiche JavaScript, Capgo è un'opzione da considerare. Offre Live Update per app CapacitorJS ed Electron, con controlli di rollout e osservabilità, permettendo ai team di affiancare unit test solidi a un percorso più sicuro per distribuire modifiche ai web bundle senza attendere la revisione degli store per ogni fix.