javascript-unit-testing-performance
This page was AI-translated by PageTurner (beta). Not officially endorsed by the project. Found an error? Report issue →
Jest kjører tusenvis av tester hos Facebook til enhver tid, enten gjennom kontinuerlig integrasjon eller manuelt utløst av utviklere under utvikling. Dette har fungert bra i årevis selv etter at de som jobbet med Jest har gått videre til andre prosjekter innen Facebook.
Etter hvert som utviklere la til flere og flere tester, la vi merke til at Jest sin ytelse ikke ville skaleres. I tillegg har JavaScript-økosystemet forandret seg dramatisk det siste året med introduksjon av teknologier som npm3 og Babel, som vi ikke hadde forutsett. Vi dannet et nytt Jest-team for å takle alle disse utfordringene, og vi vil dele fremdriften og planene våre på denne bloggen fra nå av.
Jest skiller seg litt fra de fleste testkjøringsverktøy. Vi designet den for å fungere godt i konteksten av Facebooks infrastruktur:
-
Monorepo Hos Facebook har vi et enormt monorepo som inneholder all JavaScript-koden vår. Det er mange grunner til at denne tilnærmingen er fordelaktig for oss, og det finnes en fantastisk presentasjon fra en Google-ingeniør som belyser alle fordelene og ulempene med monorepoer.
-
Sandkassekjøring En annen viktig Jest-funksjon for Facebook er hvordan den virtualiserer testmiljøet og innkapsler
requirefor å isolere kodekjøring og enkelttester. Vi jobber til og med med å gjøre Jest mer modulær slik at vi kan utnytte denne funksjonaliteten i andre sammenhenger utenfor testing. -
providesModule Hvis du har sett på noen av våre åpne JavaScript-prosjekter, kan du ha lagt merke til at vi bruker en
@providesModule-header for å tildele globalt unike ID-er til moduler. Dette krever noen tilpassede verktøy, men det lar oss referere til moduler uten relative baner – noe som har hjulpet oss å bevege oss ekstremt raskt, skalert godt etter hvert som organisasjonen vokste, og fremmet kodedeling på tvers av hele selskapet. Se RelayContainer for et eksempel på hvordan dette fungerer i praksis. En ulempe er imidlertid at vi må lese og parse hele JavaScript-kodebasen vår for å løse en enkelt require-setning. Dette ville åpenbart vært altfor kostbart uten omfattende caching, spesielt for en kortlivet prosess som Jest.
På grunn av disse unike begrensningene vil Jest kanskje aldri kunne være like rask som andre testkjøringsverktøy når hele testsuiten kjøres. Men utviklere trenger sjelden å kjøre hele testsuiten. Gjennom statisk analyse i node-haste-prosjektet har vi gjort jest --onlyChanged (eller jest -o) til standardmodus for Jest-kjøring hos Facebook. I denne modusen bygger vi en omvendt avhengighetsgraf for å finne kun de berørte testene som må kjøres basert på endrede moduler.
Optimal planlegging av testkjøringer
Statisk analyse viser som regel at flere tester må kjøres. Antallet berørte tester kan variere fra noen få til tusenvis. For å påskynde denne prosessen parallelliserer Jest testkjøringer på tvers av arbeidere. Dette er flott ettersom mesteparten av Facebooks utvikling skjer på eksterne servere med mange CPU-kjerner.
Nylig la vi merke til at Jest ofte virket fastlåst med "Venter på 3 tester" i opptil et minutt mot slutten av en kjøring. Det viste seg at vi hadde noen ekstremt trege tester i kodebasen som dominerte kjøretiden. Mens vi klarte å effektivisere disse enkelttestene betraktelig, endret vi også hvordan Jest planlegger testkjøringer. Tidligere brukte vi filsystemtraversering for planlegging, som egentlig var ganske tilfeldig. Her er et eksempel med 11 tester representert som grå blokker fordelt på to arbeidere. Blokkstørrelsen representerer testens kjøretid:
Vi kjørte tilfeldig blanding av raske og trege tester, og en av de tregeste testene ble kjørt etter at nesten alle andre tester var fullført – mens den andre arbeideren satt inaktiv.
Vi gjorde en endring som planlegger tester basert på filstørrelsen deres – dette er vanligvis en god indikator på hvor lang tid en test vil ta. En test med flere tusen kodelinjer vil sannsynligvis ta lengre tid enn en test med 15 kodelinjer. Selv om dette sparte omtrent 10% på total kjøretid, fant vi en bedre heuristikk: nå lagrer Jest kjøretiden for hver test i en hurtigbuffer, og på påfølgende kjøringer planlegger den de tregeste testene først. Dette forbedret den totale kjøretiden med omtrent 20%.
Her er et eksempel på den samme testkjøringen som før, men med bedre planlegging:
Fordi vi kjører trege tester først, kan det noen ganger virke som om Jest bruker lang tid på å starte – vi viser bare resultater etter at den første testen er fullført. I fremtiden planlegger vi å kjøre tidligere mislykkede tester først, siden det å få denne informasjonen til utviklere så raskt som mulig er viktigst.
Inline Requires og Late Mocks
Hvis du har skrevet tester med Jasmine før, ser de sannsynligvis slik ut:
const sum = require('sum');
describe('sum', () => {
it('works', () => {
expect(sum(5, 4)).toBe(9);
});
});
En spesiell ting vi gjør i Jest er å tilbakestille hele modulregisteret etter hver enkelt test (kalt it) for å sikre at tester ikke avhenger av hverandre. Før Jest var individuelle tester avhengige av hverandre, og intern modultilstand lek ofte mellom dem. Når ingeniører fjernet, omorganiserte eller refaktorerte tester, begynte noen å feile, noe som gjorde det vanskelig å forstå hva som skjedde.
Hver enkelt test i Jest mottar en helt ny kopi av alle moduler, inkludert nye versjoner av alle mockede avhengigheter som tar lang tid å generere for hver test. En bivirkning av dette er at vi måtte kalle require manuelt før hver test, slik som dette:
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);
});
});
Vi bygde en babel-transform kalt inline-requires som fjerner toppnivå require-setninger og bytter dem ut med direkte innkalling i koden. For eksempel vil linjen const sum = require('sum'); bli fjernet fra koden, men hver bruk av sum i filen vil bli erstattet med require('sum'). Med denne transformeringen kan vi skrive tester akkurat som du ville forvente i Jasmine, og koden transformeres slik:
describe('sum', () => {
it('works', () => {
expect(require('sum')(5, 4)).toBe(9);
});
});
En flott bivirkning ved inline requires er at vi kun krever modulene som faktisk brukes i selve testen, i stedet for alle modulene som ble brukt i hele filen.
En flott bivirkning av inline requires er at vi bare henter modulene vi faktisk bruker i selve testen, i stedet for alle modulene som brukes i hele filen.
Vi klarte å oppdatere alle testene ved hjelp av en codemod på null komma niks – det var en enkelt endring på 50 000 linjer kode. Inline requires og late mocks forbedret testkjøretiden med 50%.
Inline-require babel-pluginet er ikke bare nyttig for Jest, men også for vanlig JavaScript. Det ble lansert av Bhuwan til alle brukere av facebook.com for bare en uke siden og forbedret oppstartstiden betraktelig.
Foreløpig, hvis du vil bruke denne transformasjonen i Jest, må du legge den til manuelt i Babel-konfigurasjonen din. Vi jobber med måter å gjøre dette enklere å aktivere.
Foreløpig, hvis du ønsker å bruke denne transformasjonen i Jest, må du legge den til manuelt i Babel-konfigurasjonen din. Vi jobber med måter å gjøre dette enklere å aktivere.
Den åpne kildekode-versjonen av Jest pleide å være en forgrening av vår interne versjon, og vi synkroniserte Jest ut bare noen få ganger i året. Dette var en tungvint manuell prosess som krevde å fikse mange tester hver gang. Nylig oppgraderte vi Jest og skaffet likhet på alle plattformer (iOS, Android og web), og deretter aktiverte vi synkroniseringsprosessen vår. Nå blir enhver endring i Jest i åpen kildekode kjørt mot alle våre interne tester, og det finnes bare én enkelt versjon av Jest som er konsekvent overalt.
Den første funksjonen vi kunne dra nytte av etter å ha slått sammen kodebasene var preprosessorerens hurtigbuffer. Hvis du bruker Babel sammen med Jest, må Jest preprosessere hver JavaScript-fil før den kan kjøres. Vi bygde et hurtigbufferlag slik at hver fil, når den er uendret, bare trenger å transformeres én gang. Etter at vi slo sammen kodebasene, kunne vi enkelt fikse den åpne kildekodeimplementeringen og rulle den ut på Facebook. Dette ga ytterligere 50% ytelsesforbedring. Siden hurtigbufferen bare virker fra andre kjøring, ble oppstartstiden for Jest ikke påvirket.
Vi innså også at vi utførte mange filbaneoperasjoner når vi løste relative import-setninger. Fordi modulregisteret tilbakestilles for hver test, var det tusenvis av kall som kunne memoizes. En stor optimalisering var å legge til mye mer hurtigbuffering, ikke bare rundt en enkelt test, men også på tvers av testfiler. Tidligere genererte vi modulmetadata for automock-funksjonen én gang per testfil. Objektet en modul eksporterer endrer seg imidlertid aldri, så nå deler vi denne koden på tvers av testfiler. Dessverre, siden JavaScript og Node.js ikke har delt minne, må vi gjøre alt dette arbeidet minst én gang per arbeidsprosess.
Still spørsmål ved alt
Når man prøver å forbedre ytelsen, er det viktig å også dykke ned i systemene som ligger over og under ditt system. For Jest inkluderer dette for eksempel Node.js og testfilene selv. En av de første tingene vi gjorde var å oppgradere Node.js på Facebook fra den flere år gamle 0.10-versjonen til iojs og deretter til Node 4. Den nye versjonen av V8 bidro til ytelsesforbedringer og var ganske enkel å oppgradere til.
Vi la merke til at path-modulen i Node.js er treg når den utfører tusenvis av filbaneoperasjoner, noe som ble fikset i Node 5.7. Inntil vi dropper støtten for Node 4 internt på Facebook, vil vi levere vår egen versjon av fastpath-modulen.
Deretter begynte vi å stille spørsmål ved den utdaterte node-haste. Som nevnt tidligere, må hele prosjektet analyseres for @providesModule-overskrifter for å bygge en avhengighetsgraf. Da dette systemet opprinnelig ble bygget, eksisterte ikke node_modules, og vårt filsystemkryp utelukket dem ikke ordentlig.
I tidligere versjoner ville Jest faktisk lese hver enkelt fil i node_modules – noe som bidro til Jests trege oppstartstid. Da vi tok opp Jest igjen, erstattet vi hele prosjektet med en ny implementering basert på react-natives pakker. Oppstartstiden for Jest er nå under ett sekund, selv i store prosjekter. React-native-teamet, spesielt David, Amjad og Martin, gjorde en enestående jobb på dette prosjektet.
Oppsummering
Mange av endringene ovenfor forbedret testkjøretiden med 10% eller til og med 50%. Vi startet med en kjøretid på omtrent 10 minutter for alle tester, og uten disse forbedringene ville vi sannsynligvis vært på rundt 20 minutter nå. Etter disse forbedringene tar det nå konsekvent rundt 1 minutt og 35 sekunder å kjøre alle testene våre!
Enda viktigere er at nye tester får total kjøretid til å vokse veldig sakte. Utviklere kan skrive og kjøre flere tester uten å merke kostnadene.
Med Jests nylige 0.9-utgivelse og ytelsesforbedringer fra node-haste2-integrasjonen, gikk kjøretiden for Relay-rammeverkets testsett ned fra 60 sekunder til omtrent 25, og react-native-testsettet fullføres nå på under ti sekunder på en 13-tommers MacBook Pro.
Vi er veldig fornøyde med gevinstene vi har sett så langt, og vi kommer til å fortsette å jobbe med Jest og gjøre det bedre. Hvis du er nysgjerrig på å bidra til Jest, kan du gjerne ta kontakt på GitHub, Discord eller Facebook :)
