Test unitario: La rete di sicurezza ovvero come dormire la notte
La Code Coverage al 100% è una metrica di vanità. Come scrivere unit test significativi con Jest che rilevano bug reali senza impedire il refactoring.
Perché Maison Code ne parla
In Maison Code Paris, agiamo come la coscienza architettonica dei nostri clienti. Spesso ereditiamo stack “moderni” costruiti senza una comprensione fondamentale della scala.
Discutiamo di questo argomento perché rappresenta un punto di svolta critico nella maturità ingegneristica. Implementarlo correttamente differenzia un MVP fragile da una piattaforma resiliente di livello aziendale.
La crisi di fiducia
È necessario eseguire il refactoring di una funzione principale.
Forse è la funzione calculateTax() nella cassa.
È disordinato. Ha nidificate istruzioni “if”. Utilizza la vecchia sintassi. Vuoi ripulirlo.
Ma sei terrorizzato.
Ti chiedi: “Se tocco questo e infrango una regola fiscale specifica per i clienti all’ingrosso tedeschi, lo saprò?”
Se la risposta è “No”, sei paralizzato.
Quindi lascialo. Il “Codice Legacy” rimane. Marcisce. Diventa la “cartella spaventosa” che nessuno tocca.
Gli Unit Test sono la cura per questa paralisi.
Congelano il comportamento del codice nel tempo.
Garantiscono: “Questa funzione, dato l’input 2, DEVE restituire 4.”
Se il contratto regge, puoi riscrivere l’implementazione interna come preferisci.
I test facilitano il Refactoring aggressivo. Senza test, il refactoring è solo “indovinare”.
Perché Maison Code parla di test unitari
Noi di Maison Code eseguiamo spesso “missioni di salvataggio” su piattaforme legacy. Troviamo codebase in cui gli sviluppatori hanno paura di implementare. “Non toccare il modulo UserSync, si rompe se lo guardi male.” Questa paura paralizza il business. Le nuove funzionalità richiedono mesi perché l’80% del tempo viene dedicato ai test di regressione manuale. Riteniamo che il codice testabile sia un codice pulito. Implementiamo solide suite di unit test utilizzando Jest/Vitest per restituire fiducia al team. Quando i test diventano verdi, esegui la distribuzione. Semplice proprio così. Trasformiamo il “Deployment Friday” da un incubo in un non-evento.
Lo strumento: Jest/Vitest
- Jest: L’incumbent. Mantenuto da Meta. Standard nell’app Crea React/Next.js. Potente ma pesante.
- Vitest: Lo sfidante. Realizzato da Vite. È funzionalmente identico a Jest (API compatibile) ma 10 volte più veloce perché utilizza i moduli ES in modo nativo. Utilizzeremo la sintassi Jest qui, poiché si applica a entrambi. I concetti sono universali.
La strategia: cosa testare?
È qui che le squadre falliscono. La trappola della “copertura al 100%”. I manager desiderano una “copertura del codice al 100%”. Lo hanno inserito negli OKR. Ciò porta a test inutili.
Test errato (testare il framework):
const Pulsante = ({ etichetta }) => <pulsante>{etichetta}</pulsante>;
test('Il pulsante viene visualizzato', () => {
render(<etichetta del pulsante="Vai" />);
wait(screen.getByText('Go')).toBeInTheDocument();
});
Questo test ha Valore basso. Sta testando React. Sappiamo che React funziona.
Ha Manutenzione elevata. Se rinominiamo label in text, il test si interrompe, anche se l’app funziona. Questo è un “falso negativo”.
Buona strategia di test: Concentrati sulla logica aziendale e sui casi limite.
1. Funzioni pure (ROI più alto)
Una funzione pura dipende solo da argomenti e restituisce un valore. Nessun effetto collaterale. Nessuna chiamata API. Sono una gioia da testare. Corrono in millisecondi.
// logica.ts
/**
* Calcola lo sconto in base al livello del cliente.
*Regole:
* - VIP ottiene uno sconto del 20%.
* - Il prezzo negativo genera un errore.
* - Il dipendente riceve uno sconto del 50%.
*/
funzione di esportazione calcolaSconto(prezzo: numero, tipo: 'VIP' | 'Normale' | 'Dipendente'): numero {
if (prezzo < 0) lancia un nuovo errore ('Il prezzo negativo è impossibile');
if (tipo === 'Dipendente') return prezzo * 0,5;
if (tipo === 'VIP') restituisce prezzo * 0,8;
prezzo di ritorno;
}
// logica.test.ts
description('calcolasconto', () => {
test('offre il 20% di sconto ai VIP', () => {
// Disporre
prezzo costante = 100;
tipo const = 'VIP';
// Agisci
risultato const = calcolaSconto(prezzo, tipo);
// Afferma
aspettarsi(risultato).toBe(80);
});
test('offre uno sconto del 50% al dipendente', () => {
aspettarsi(calculateDiscount(100, 'Dipendente')).toBe(50);
});
test('offre uno sconto dello 0% a Regular', () => {
aspetta(calculateDiscount(100, 'Regular')).toBe(100); // 100 * 1 = 100
});
test('genera un prezzo negativo', () => {
// Nota: racchiudiamo il codice chiamante in una funzione in modo che Jest possa rilevare l'errore
wait(() => calcolaDiscount(-10, 'Normale')).toThrow('Prezzo negativo');
});
});
Questo è robusto. Documenta le regole aziendali meglio dei commenti.
2. Derisione (il male necessario)
La maggior parte del codice interagisce con il mondo (database, API, LocalStorage). Non è possibile eseguire una vera chiamata API in uno unit test. È lento, instabile e richiede credenziali. Deridi la dipendenza.
//profiloutente.ts
import { fetchUserFromStripe } da './api';
esporta la funzione asincrona getUserStatus(id: string) {
const utente = attendono fetchUserFromStripe(id); // Dipendenza esterna
if (user.delinquent) restituisce 'Bloccato';
ritorna 'Attivo';
}
//profiloutente.test.ts
import { fetchUserFromStripe } da './api';
jest.mock('./api'); // Derisione automatica del modulo. Sostituisce le funzioni reali con jest.fn()
test('restituisce Bloccato se l'utente è delinquente', async() => {
// Imposta la risposta fittizia
(fetchUserFromStripe as jest.Mock).mockResolvedValue({ id: '1', delinquent: true });
stato const = attendono getUserStatus('1');
aspetta(status).toBe('Bloccato');
// Verifica che l'API sia stata richiamata correttamente per garantire il passaggio dell'ID
wait(fetchUserFromStripe).toHaveBeenCalledWith('1');
});
Avvertenza sui mock: i mock mentono.
Se il vero fetchUserFromStripe cambia il suo formato di ritorno (ad esempio, delinquent diventa isDelinquent), il tuo test verrà comunque superato (perché hai deriso il vecchio formato), ma la tua app andrà in crash.
Utilizza TypeScript per mantenere i mock sincronizzati con i tipi reali.
Test delle istantanee: funzionalità o bug?
Jest ha introdotto “Istantanee”.
expect(component).toMatchSnapshot().
Serializza il componente renderizzato in un file di testo (__snapshots__/comp.test.js.snap).
Se si modifica H1 in H2, il test fallisce.
Il problema: gli sviluppatori lo considerano un fastidio.
Eseguono il test. Fallisce. Digitano “jest -u” (Aggiornamento) senza guardare.
I test delle istantanee sono fragili.
Best practice: utilizza le istantanee per componenti molto piccoli e stabili (icone, token di progettazione) in cui qualsiasi modifica è sospetta. Non utilizzarli per pagine complesse.
Test del codice asincrono (la trappola Async/Await)
Testare Promises è complicato. Errore comune: dimenticare “await” o “params”.
// CATTIVO: il test termina prima che la Promessa si risolva
test('test asincrono errato', () => {
fetchData().then(dati => {
wait(data).toBe('burro di arachidi');
});
});
Se fetchData fallisce, il test potrebbe comunque passare (falso positivo) o scadere.
Buono:
test('buon test asincrono', async() => {
const data = attendono fetchData();
wait(data).toBe('burro di arachidi');
});
Per i timer (ad es. setTimeout), utilizzare Timer falsi:
jest.useFakeTimers().
“jest.advanceTimersByTime(1000)”.
Ciò consente di testare un ritardo di 10 secondi in 1 millisecondo.
Il dibattito sul TDD (Test Driven Development).
“Scrivi prima il test.” Pro: obbliga a progettare l’API prima dell’implementazione. Il risultato è un codice molto pulito e disaccoppiato. Contro: velocità iniziale più lenta. Difficile quando stai “esplorando” (prototipando) e non conosci la struttura finale. Verdetto: utilizzare TDD per una logica algoritmica complessa (ad esempio, analisi di un CSV, calcolo delle imposte, manipolazione di stringhe). Non utilizzarlo per componenti dell’interfaccia utente semplici in cui si esegue l’iterazione sui pixel.
Integrazione vs Unità: la forma del trofeo
Kent C. Dodds ha formalizzato il “Trofeo Testing”.
- Analisi statica (ESLint, TypeScript): rileva errori di battitura. (Il più veloce/economico).
- Test unitari: testare funzioni pure. (Veloce).
- Test di integrazione: testare la connessione tra i componenti. (Il punto dolce).
- Test E2E: prova il browser completo. (Lento/costoso).
Raccomandazione: scrivere principalmente Test di integrazione.
Prova LoginForm + SubmitButton + ApiMock.
Non testare SubmitButton in modo isolato.
Verificare che “facendo clic su Invia si richiama l’API di accesso”.
Domande frequenti
D: Come posso testare le funzioni private? R: Non farlo. Testare l’API pubblica. La funzione privata è un dettaglio di implementazione. Se provi le funzioni private, ti blocchi in tale implementazione. Perderai la possibilità di effettuare il refactoring.
D: Integrazione CI/CD?
R: I test devono essere eseguiti su ogni richiesta pull.
Se npm test fallisce, il pulsante Unisci dovrebbe essere disabilitato.
Ciò mantiene la politica del “Green Master”.
Conclusione
I test sono un investimento. Paghi in anticipo (Tempo). Ottieni dividendi per sempre (stabilità, velocità di refactoring, documentazione). Una codebase senza test è una “Legacy Codebase” fin dal primo giorno. Una codebase con test è una “risorsa”. Apprezza di valore.
Implementazione paralizzata?
Se il tuo team è paralizzato dalla paura di violare il codice legacy, Maison Code è la soluzione. Eseguiamo “Refactoring Raid” in cui controlliamo la tua base di codice, aggiungiamo copertura di test e ripuliamo il debito tecnologico che ti sta rallentando. Formiamo il tuo team su come scrivere test che non facciano schifo.