javascript-unit-testing-performance
Cette page a été traduite par PageTurner AI (bêta). Non approuvée officiellement par le projet. Vous avez trouvé une erreur ? Signaler un problème →
Jest exécute en permanence des milliers de tests chez Facebook, soit via l'intégration continue, soit lancé manuellement par les ingénieurs pendant le développement. Cela a bien fonctionné pendant des années, même après que les personnes travaillant sur Jest ont rejoint d'autres projets chez Facebook.
Cependant, à mesure que les ingénieurs ajoutaient de plus en plus de tests, nous avons remarqué que les performances de Jest ne seraient pas extensibles. De plus, l'écosystème JavaScript a radicalement changé cette dernière année avec l'arrivée d'outils comme npm3 et Babel, que nous n'avions pas anticipés. Nous avons formé une nouvelle équipe Jest pour résoudre ces problèmes et partagerons désormais nos avancées et plans sur ce blog.
Jest diffère un peu de la plupart des lanceurs de tests. Nous l'avons conçu pour fonctionner dans le contexte de l'infrastructure de Facebook :
-
Monorepo Chez Facebook, nous disposons d'un énorme monorepo contenant tout notre code JavaScript. Cette approche présente de nombreux avantages pour nous, et il existe une excellente conférence d'un ingénieur Google qui détaille les bénéfices et inconvénients des monorepos.
-
Sandboxing Une autre fonctionnalité importante pour Facebook est la virtualisation de l'environnement de test et le wrapping de
requirepour sandboxer l'exécution du code et isoler les tests individuels. Nous travaillons même à rendre Jest plus modulaire pour exploiter cette fonctionnalité dans d'autres cas d'usage non liés aux tests. -
providesModule Si vous avez examiné nos projets JavaScript open source, vous avez peut-être remarqué l'en-tête
@providesModuleattribuant des ID globaux uniques aux modules. Cela nécessite des outils spécifiques, mais permet de référencer des modules sans chemins relatifs, accélérant considérablement notre développement, évoluant avec notre croissance et favorisant le partage de code à l'échelle de l'entreprise. Consultez RelayContainer pour un exemple pratique. L'inconvénient est que nous devons lire et parser l'intégralité de notre codebase pour résoudre une seule instruction require, ce qui serait prohibitif sans cache étendu, surtout pour un processus éphémère comme Jest.
Ces contraintes uniques font que Jest ne sera peut-être jamais aussi rapide que d'autres lanceurs de tests sur notre suite complète. Cependant, les ingénieurs exécutent rarement tous les tests. Grâce à l'analyse statique du projet node-haste, le mode par défaut chez Facebook est désormais jest --onlyChanged (jest -o). Dans ce mode, nous construisons un graphe de dépendances inversé pour exécuter uniquement les tests impactés par les modules modifiés.
Planification optimale d'une exécution de tests
Souvent, notre analyse statique détermine qu'il faut exécuter plusieurs tests. Le nombre peut varier de quelques-uns à des milliers. Pour accélérer le processus, Jest parallélise les tests sur plusieurs workers. C'est idéal car le développement chez Facebook s'effectue principalement sur des serveurs distants dotés de nombreux cœurs CPU.
Récemment, nous remarquions que Jest semblait bloqué sur "Waiting for 3 tests" pendant une minute en fin d'exécution. Certains tests très lents dominaient le temps d'exécution. Après les avoir optimisés, nous avons modifié la planification des tests. Auparavant basée sur une traversée aléatoire du système de fichiers, voici un exemple avec 11 tests (blocs gris) répartis sur deux workers (la taille indique la durée) :
Nous exécutions aléatoirement des tests rapides et lents, et nos tests les plus lents finissaient par s'exécuter lorsque tous les autres étaient terminés, laissant le second worker inactif.
Nous avons modifié la planification des tests en nous basant sur la taille des fichiers, qui est généralement un bon indicateur de la durée d'exécution d'un test. Un test de plusieurs milliers de lignes prendra probablement plus de temps qu'un test de 15 lignes. Bien que cette méthode ait accéléré l'exécution complète des tests d'environ 10%, nous avons finalement trouvé une meilleure heuristique : désormais Jest stocke le temps d'exécution de chaque test dans un cache et lors des exécutions suivantes, il planifie d'abord les tests les plus lents. Globalement, cela a permis d'améliorer le temps d'exécution de tous les tests d'environ 20%.
Voici un exemple du même test qu'auparavant avec une meilleure planification :
Comme nous exécutons d'abord les tests lents, Jest peut parfois sembler prendre longtemps à démarrer - nous n'affichons les résultats qu'après la fin du premier test. À l'avenir, nous prévoyons d'exécuter d'abord les tests précédemment échoués, car fournir cette information aux développeurs le plus rapidement possible est primordial.
Requires Inline et Mocking Paresseux
Si vous avez déjà écrit des tests avec Jasmine, ils ressemblent probablement à ceci :
const sum = require('sum');
describe('sum', () => {
it('works', () => {
expect(sum(5, 4)).toBe(9);
});
});
Une particularité de Jest est la réinitialisation complète du registre des modules après chaque test (appel à it), garantissant ainsi l'indépendance des tests entre eux. Avant Jest, les tests individuels présentaient des dépendances mutuelles et les états internes des modules interféraient fréquemment. Lorsque les ingénieurs supprimaient, réorganisaient ou restructuraient des tests, certains échouaient sans raison évidente, rendant la situation difficile à diagnostiquer.
Chaque test dans Jest reçoit une toute nouvelle copie de tous les modules, y compris de nouvelles versions des dépendances mockées dont la génération prend beaucoup de temps pour chaque test. Un effet secondaire de ce fonctionnement est que nous devions appeler require manuellement avant chaque test, comme ceci :
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);
});
});
Nous avons construit une transformation Babel appelée inline-requires qui supprime les instructions require de haut niveau et les incorpore directement dans le code. Par exemple, la ligne const sum = require('sum'); sera supprimée du code, mais chaque utilisation de sum dans le fichier sera remplacée par require('sum'). Grâce à cette transformation, nous pouvons écrire des tests comme vous vous y attendriez dans Jasmine, et le code est transformé ainsi :
describe('sum', () => {
it('works', () => {
expect(require('sum')(5, 4)).toBe(9);
});
});
Un excellent effet secondaire des require inline est que nous ne chargeons que les modules que nous utilisons réellement dans le test lui-même, au lieu de tous les modules utilisés dans le fichier entier.
Un avantage majeur des requires inline est que nous ne chargeons que les modules réellement utilisés dans le test lui-même, et non tous les modules du fichier.
Nous avons pu mettre à jour tous les tests en un rien de temps grâce à un codemod - un changement de code simple de 50 000 lignes. Les requires inline et le mocking paresseux ont amélioré le temps d'exécution des tests de 50%.
Le plugin Babel inline-require n'est pas seulement utile pour Jest mais aussi pour le JavaScript classique. Bhuwan l'a déployé pour tous les utilisateurs de facebook.com il y a une semaine, améliorant significativement le temps de démarrage.
Pour l'instant, si vous souhaitez utiliser cette transformation dans Jest, vous devrez l'ajouter manuellement à votre configuration Babel. Nous travaillons à simplifier cette adoption.
Synchronisation des dépôts et mise en cache
La version open source de Jest était autrefois un fork de notre version interne, que nous ne synchronisions que tous les quelques mois. Ce processus manuel était laborieux et nécessitait de corriger de nombreux tests à chaque fois. Nous avons récemment mis à niveau Jest pour obtenir la parité sur toutes les plateformes (iOS, Android et web) puis activé notre processus de synchronisation. Désormais, chaque modification de Jest en open source est testée contre tous nos tests internes, et une seule version cohérente de Jest existe partout.
Le premier avantage que nous avons pu exploiter après la fusion a été le cache de pré-traitement. Si vous utilisez Babel avec Jest, Jest doit pré-traiter chaque fichier JavaScript avant de pouvoir l'exécuter. Nous avons construit une couche de cache pour que chaque fichier, lorsqu'il n'est pas modifié, ne soit transformé qu'une seule fois. Après la fusion de Jest, nous avons pu facilement corriger l'implémentation open source et l'avons déployée chez Facebook. Cela a entraîné un gain supplémentaire de 50% en performance. Comme le cache ne fonctionne qu'à partir du deuxième lancement, le temps de démarrage initial de Jest reste inchangé.
Nous avons aussi réalisé que nous effectuions de nombreuses opérations de chemin lors de la résolution des imports relatifs. Comme le registre des modules est réinitialisé pour chaque test, des milliers d'appels pouvaient être mémorisés. Une optimisation majeure a été d'ajouter beaucoup plus de cache, non seulement autour d'un seul test, mais aussi entre les fichiers de test. Auparavant, nous générions les métadonnées de module pour la fonctionnalité d'auto-mock une fois par fichier de test. Or l'objet exporté par un module ne change jamais, donc nous partageons maintenant ce code entre les fichiers de test. Malheureusement, comme JavaScript et Node.js n'ont pas de mémoire partagée, nous devons effectuer ce travail au moins une fois par processus worker.
Tout remettre en question
Lorsqu'on cherche à améliorer les performances, il est crucial d'examiner aussi les systèmes qui encadrent le vôtre. Dans le cas de Jest, par exemple Node.js et les fichiers de test eux-mêmes. Une de nos premières actions a été de mettre à jour Node.js chez Facebook depuis l'ancienne version 0.10 vers iojs puis vers Node 4. La nouvelle version de V8 a aidé à améliorer les performances et fut assez simple à mettre à jour.
Nous avons remarqué que le module path de Node.js est lent lors de milliers d'opérations sur les chemins, ce qui a été corrigé dans Node 5.7. Jusqu'à ce que nous abandonnions le support de Node 4 en interne chez Facebook, nous fournirons notre propre version du module fastpath.
Nous avons ensuite commencé à questionner l'obsolescence de node-haste. Comme mentionné précédemment, tout le projet doit être analysé pour les en-têtes @providesModule afin de construire un graphe de dépendances. Quand ce système a été conçu initialement, node_modules n'existait pas et notre crawler de fichiers ne les excluait pas correctement.
Dans les versions précédentes, Jest lisait en réalité chaque fichier dans node_modules - ce qui contribuait au démarrage lent de Jest. Lorsque nous avons repris Jest, nous avons remplacé tout le projet par une nouvelle implémentation basée sur le packager de react-native. Le temps de démarrage de Jest est désormais inférieur à une seconde, même sur de grands projets. L'équipe react-native, en particulier David, Amjad et Martin, a fait un travail remarquable sur ce projet.
Additionner les gains
Beaucoup de ces améliorations ont réduit le temps d'exécution des tests de 10% voire parfois 50%. Nous avons commencé avec un temps d'exécution d'environ 10 minutes pour tous les tests, et sans ces améliorations nous serions probablement autour de 20 minutes maintenant. Après ces optimisations, l'exécution complète prend systématiquement environ 1 minute et 35 secondes !
Plus important encore, l'ajout de nouveaux tests fait croître le temps total très lentement. Les ingénieurs peuvent écrire et exécuter plus de tests sans en subir les coûts.
Avec la récente version 0.9 de Jest et les améliorations de performance de l'intégration node-haste2, le temps d'exécution de la suite de tests du framework Relay est passé de 60 secondes à environ 25, et la suite de tests de react-native se termine désormais en moins de dix secondes sur un MacBook Pro 13".
Nous sommes très satisfaits des gains obtenus jusqu'ici, et nous continuerons à travailler sur Jest pour l'améliorer. Si vous souhaitez contribuer à Jest, n'hésitez pas à nous contacter sur GitHub, Discord ou Facebook :)
