javascript-unit-testing-performance
Questa pagina è stata tradotta da PageTurner AI (beta). Non ufficialmente approvata dal progetto. Hai trovato un errore? Segnala problema →
Jest esegue costantemente migliaia di test su Facebook, sia tramite integrazione continua che su invocazione manuale degli ingegneri durante lo sviluppo. Questo approccio ha funzionato bene per anni, anche quando le persone che lavoravano a Jest sono passate ad altri progetti all'interno di Facebook.
Tuttavia, con l'aggiunta progressiva di sempre più test, abbiamo notato che le prestazioni di Jest non erano scalabili. Inoltre, nell'ultimo anno l'ecosistema JavaScript è cambiato radicalmente con l'introduzione di strumenti come npm3 e Babel, che non avevamo previsto. Abbiamo formato un nuovo team Jest per affrontare queste problematiche e condivideremo i nostri progressi e piani su questo blog da ora in poi.
Jest è leggermente diverso dalla maggior parte dei test runner. Lo abbiamo progettato per funzionare bene nel contesto dell'infrastruttura di Facebook:
-
Monorepo In Facebook abbiamo un enorme monorepo che contiene tutto il nostro codice JavaScript. Ci sono molte ragioni per cui questo approccio è vantaggioso per noi ed esiste un talk eccezionale di un ingegnere di Google che illustra tutti i vantaggi e gli svantaggi dei monorepo.
-
Sandboxing Un'altra caratteristica importante di Jest per Facebook è come virtualizza l'ambiente di test e avvolge
requireper sandboxare l'esecuzione del codice e isolare i singoli test. Stiamo persino lavorando per rendere Jest più modulare così da poter sfruttare questa funzionalità in altri casi d'uso non legati ai test. -
providesModule Se hai esaminato i nostri progetti JavaScript open source, potresti aver notato che utilizziamo un header
@providesModuleper assegnare ID globalmente univoci ai moduli. Questo richiede strumenti personalizzati, ma ci permette di referenziare moduli senza percorsi relativi, aiutandoci a muoverci estremamente velocemente, scalare bene con la crescita dell'organizzazione e favorire la condivisione del codice in tutta l'azienda. Guarda RelayContainer per un esempio pratico. Uno svantaggio di questo approccio è che siamo costretti a leggere e analizzare l'intera codebase JavaScript per risolvere una singola dichiarazione require. Senza un'estesa cache, questo sarebbe proibitivamente costoso, specialmente per un processo a breve termine come Jest.
A causa di questi vincoli unici, Jest potrebbe non raggiungere mai la stessa velocità di altri test runner quando esegue l'intera suite di test. Tuttavia, raramente gli ingegneri devono eseguire l'intera suite. Grazie all'analisi statica del progetto node-haste, abbiamo reso la modalità predefinita per eseguire Jest su Facebook jest --onlyChanged o jest -o. In questa modalità costruiamo un grafico delle dipendenze inverso per trovare solo i test interessati da eseguire in base ai moduli modificati.
Pianificazione Ottimale di una Esecuzione di Test
Spesso la nostra analisi statica determina che devono essere eseguiti più test. Il numero di test interessati può variare da pochi a migliaia. Per velocizzare il processo, Jest parallelizza le esecuzioni dei test tra diversi worker. Questo è ottimo perché gran parte dello sviluppo di Facebook avviene su server remoti con molti core CPU.
Recentemente abbiamo notato che Jest spesso sembrava bloccato con "Waiting for 3 tests" per quasi un minuto verso la fine dell'esecuzione. Abbiamo scoperto che alcuni test molto lenti nella nostra codebase dominavano il tempo di esecuzione. Oltre ad accelerare questi singoli test, abbiamo modificato come Jest pianifica le esecuzioni. In precedenza usavamo un attraversamento casuale del file system per pianificare i test. Ecco un esempio di 11 test rappresentati da blocchi grigi su due worker, dove la dimensione del blocco indica la durata del test:
Eseguivamo casualmente un mix di test veloci e lenti, e uno dei nostri test più lenti veniva eseguito quando quasi tutti gli altri erano completati, lasciando il secondo worker inattivo.
Abbiamo modificato la pianificazione dei test basandoci sulle dimensioni dei file, che solitamente sono un buon indicatore della durata di un test. Un test con migliaia di righe di codice probabilmente richiede più tempo di uno con solo 15 righe. Sebbene questo abbia accelerato l'intera esecuzione del test di circa il 10%, abbiamo successivamente trovato un'euristica migliore: ora Jest memorizza la durata di ogni test in una cache e nelle esecuzioni successive pianifica per primi i test più lenti. Complessivamente, questo ha migliorato il tempo di esecuzione di tutti i test di circa il 20%.
Ecco un esempio della stessa esecuzione di test precedente con una pianificazione migliorata:
Poiché eseguiamo prima i test più lenti, a volte Jest può sembrare impiegare molto tempo per avviarsi - stampiamo i risultati solo dopo il completamento del primo test. Per il futuro stiamo pianificando di eseguire prima i test precedentemente falliti, poiché fornire queste informazioni agli sviluppatori il più rapidamente possibile è prioritario.
Require in linea e mocking lazy
Se hai scritto test con Jasmine in precedenza, probabilmente hanno questo aspetto:
const sum = require('sum');
describe('sum', () => {
it('works', () => {
expect(sum(5, 4)).toBe(9);
});
});
Una cosa speciale che facciamo in Jest è resettare l'intero registro dei moduli dopo ogni singolo test (chiamata a it) per assicurarci che i test non dipendano l'uno dall'altro. Prima di Jest, i singoli test dipendevano l'uno dall'altro e lo stato interno del modulo spesso trapelava tra di essi. Quando gli ingegneri rimuovevano, riordinavano o rifattorizzavano i test, alcuni di essi iniziavano a fallire, rendendo difficile per le persone capire cosa stesse succedendo.
Ogni singolo test in Jest riceve una copia nuova di tutti i moduli, incluse nuove versioni delle dipendenze mockate che richiedono molto tempo per essere generate per ogni test. Un effetto collaterale è che dovevamo chiamare require manualmente prima di ogni test, così:
let sum;
describe('sum', () => {
beforeEach(() => {
sum = require('sum');
});
it('works', () => {
expect(sum(5, 4)).toBe(9);
});
it('works too', () => {
// This copy of sum is not the same as in the previous call to `it`.
expect(sum(2, 3)).toBe(5);
});
});
Abbiamo costruito una trasformazione Babel chiamata inline-requires che rimuove le dichiarazioni require di livello superiore e le inlina nel codice. Ad esempio, la riga const sum = require('sum'); verrà rimossa dal codice, ma ogni uso di sum nel file verrà sostituito con require('sum'). Con questa trasformazione possiamo scrivere test proprio come ci si aspetterebbe in Jasmine e il codice viene trasformato in questo:
describe('sum', () => {
it('works', () => {
expect(require('sum')(5, 4)).toBe(9);
});
});
Un grande effetto collaterale delle richieste inline è che richiediamo solo i moduli che effettivamente utilizziamo all'interno del test stesso, invece di tutti i moduli utilizzati nell'intero file.
Un ottimo effetto collaterale dei require in linea è che richiediamo solo i moduli effettivamente utilizzati nel test stesso, invece di tutti i moduli usati nell'intero file.
Siamo riusciti ad aggiornare tutti i test usando un codemod in pochissimo tempo – è stato un cambiamento di codice semplice da 50.000 righe. I require in linea e il mocking lazy hanno migliorato il tempo di esecuzione dei test del 50%.
La trasformazione inline-require di Babel non è utile solo per Jest ma anche per JavaScript normale. È stata distribuita da Bhuwan a tutti gli utenti di facebook.com appena una settimana fa e ha migliorato significativamente il tempo di avvio.
Per ora, se desideri utilizzare questa trasformazione in Jest, dovrai aggiungerla manualmente alla tua configurazione Babel. Stiamo lavorando per rendere più semplice l'adozione.
Sincronizzazione del repository e caching
La versione open source di Jest era un fork della nostra versione interna, e sincronizzavamo il codice solo ogni pochi mesi. Era un processo manuale complesso che richiedeva la correzione di molti test ogni volta. Recentemente abbiamo aggiornato Jest e portato la parità su tutte le piattaforme (iOS, Android e web), abilitando poi il nostro processo di sincronizzazione. Ora ogni modifica a Jest in open source viene eseguita su tutti i nostri test interni, e c'è una singola versione di Jest coerente ovunque.
La prima funzionalità che abbiamo sfruttato dopo aver riunificato il fork è stata la cache del preprocessore. Se utilizzi Babel insieme a Jest, Jest deve pre-elaborare ogni file JavaScript prima di poterlo eseguire. Abbiamo implementato un livello di caching che garantisce che ogni file, se invariato, venga trasformato una sola volta. Dopo aver riunificato Jest, abbiamo potuto correggere facilmente l'implementazione open source e integrarla in Facebook. Questo ha portato a un ulteriore miglioramento delle prestazioni del 50%. Poiché la cache funziona solo dalla seconda esecuzione, il tempo di avvio a freddo di Jest non è stato influenzato.
Abbiamo anche notato che eseguivamo molte operazioni sui percorsi durante la risoluzione dei require relativi. Poiché il registro dei moduli viene reimpostato per ogni test, erano presenti migliaia di chiamate che potevano essere memorizzate. Una grande ottimizzazione è stata l'aggiunta di molta più cache, non solo per singoli test ma anche tra file di test. In precedenza, generavamo i metadati del modulo per la funzionalità di automocking una volta per ogni file di test. L'oggetto esportato da un modulo però non cambia mai, quindi ora condividiamo questo codice tra i file di test. Purtroppo, poiché JavaScript e Node.js non hanno memoria condivisa, dobbiamo eseguire tutto questo lavoro almeno una volta per processo worker.
Mettere tutto in discussione
Quando si cerca di migliorare le prestazioni, è importante analizzare anche i sistemi sottostanti e sovrastanti. Nel caso di Jest, elementi come Node.js e gli stessi file di test. Una delle prime cose che abbiamo fatto è stato aggiornare Node.js in Facebook dalla vecchia versione 0.10 a iojs e successivamente a Node 4. La nuova versione di V8 ha migliorato le prestazioni ed è stato abbastanza semplice aggiornare.
Abbiamo notato che il modulo path in Node.js è lento quando esegue migliaia di operazioni sui percorsi, problema risolto in Node 5.7. Fino a quando non elimineremo il supporto per Node 4 internamente a Facebook, distribuiremo la nostra versione del modulo fastpath.
Abbiamo poi iniziato a mettere in discussione l'obsoleto node-haste. Come accennato prima, l'intero progetto deve essere analizzato per gli header @providesModule per costruire un grafo delle dipendenze. Quando questo sistema è stato originariamente creato, node_modules non esisteva e il nostro crawler del file system non li escludeva correttamente.
Nelle versioni precedenti, Jest leggeva effettivamente ogni file in node_modules - contribuendo al lento tempo di avvio di Jest. Quando abbiamo ripreso Jest, abbiamo sostituito l'intero progetto con una nuova implementazione basata sul packager di react-native. Il tempo di avvio di Jest è ora inferiore a un secondo anche su progetti grandi. Il team di react-native, in particolare David, Amjad e Martin, ha fatto un lavoro eccezionale su questo progetto.
Sommando i risultati
Molti dei cambiamenti sopra hanno migliorato il runtime dei test del 10% o talvolta anche del 50%. Siamo partiti da un runtime di circa 10 minuti per tutti i test, e senza questi miglioramenti saremmo probabilmente a circa 20 minuti ora. Dopo questi miglioramenti, invece, ora impiega costantemente circa 1 minuto e 35 secondi per eseguire tutti i nostri test!
Soprattutto, l'aggiunta di nuovi test fa crescere il runtime totale molto lentamente. Gli ingegneri possono scrivere ed eseguire più test senza risentire dei costi.
Con la recente versione 0.9 di Jest e i miglioramenti prestazionali dell'integrazione node-haste2, il runtime della suite di test del framework Relay è sceso da 60 secondi a circa 25, mentre la suite di test di react-native ora termina in meno di dieci secondi su un MacBook Pro da 13 pollici.
Siamo molto soddisfatti dei risultati ottenuti finora e continueremo a lavorare su Jest per renderlo migliore. Se sei interessato a contribuire a Jest, contattaci pure su GitHub, Discord o Facebook :)
