Nous écrivons des tests d'intégration frontaux et accélérons les versions

salut! Je m'appelle Vova, je suis frontender à Tinkoff. Notre équipe est responsable de deux produits pour les personnes morales. Concernant la taille du produit, je peux dire en chiffres: une régression complète de chaque produit par deux testeurs prend trois jours (sans l'influence de facteurs externes).



Les termes sont importants et implorent de les combattre. Il existe plusieurs façons de se battre, les principales sont:



  • Sciez les applications en produits plus petits avec leurs cycles de libĂ©ration.

  • Couverture du produit avec des tests conformes Ă  la pyramide des tests.



Le dernier paragraphe Ă©tait le sujet de mon article.



image



Test de la pyramide



Comme nous le savons, il existe trois niveaux dans la pyramide des tests: les tests unitaires, les tests d'intégration et les tests e2e. Je pense que beaucoup sont familiers avec les unités, ainsi qu'avec e2e, donc je vais m'attarder sur les tests d'intégration un peu plus en détail.



Dans le cadre des tests d'intégration, nous vérifions le fonctionnement de l'ensemble de l'application via l'interaction avec l'interface utilisateur, cependant, la principale différence avec les tests e2e est que nous ne faisons pas de vraies demandes de support. Ceci est fait afin de vérifier uniquement l'interaction de tous les systèmes à l'avant, afin de réduire le nombre de tests e2e dans le futur.



Nous utilisons Cypress pour écrire des tests d'intégration . Dans cet article, je ne le comparerai pas avec d'autres frameworks, je dirai seulement pourquoi nous nous sommes retrouvés avec:



  1. Documentation très détaillée.

  2. Débogage facile des tests (Cypress a créé une interface graphique spéciale pour cela avec des étapes de voyage dans le temps dans le test).



Ces points étaient importants pour notre équipe, car nous n'avions pas d'expérience dans la rédaction de tests d'intégration et un démarrage très simple était nécessaire. Dans cet article, je veux parler du chemin que nous avons parcouru, des bosses que nous avons comblées et de partager des recettes pour la mise en œuvre.



Le début du chemin



Au tout début, Angular Workspace avec une seule application était utilisé pour organiser le code. Après avoir installé le package Cypress, le dossier cypress avec la configuration et les tests est apparu à la racine de l'application, nous nous sommes arrêtés sur cette option. Lorsque nous essayons de préparer un script dans package.json qui est requis pour exécuter l'application et exécuter des tests par-dessus, nous avons rencontré les problèmes suivants:



  1. Dans index.html, certains scripts qui n'étaient pas nécessaires dans les tests d'intégration ont été cousus.

  2. Pour exécuter les tests d'intégration, il fallait s'assurer que le serveur avec l'application était en cours d'exécution.



Le problème avec index.html a été résolu par une configuration de construction distincte - appelons-la sypress - dans laquelle nous avons spécifié un index.html personnalisé. Comment mettre en œuvre cela? Recherchez la configuration de votre application dans angular.json, ouvrez la section de construction, ajoutez-y une configuration distincte pour Cypress et n'oubliez pas de spécifier cette configuration pour le mode de service.



Exemple de configuration pour la construction:



"build": {
 ...
 "configurations": {
   â€¦ //  
   "cypress": {
     "aot": true,
     "index": "projects/main-app-integrations/src/fixtures/index.html",
     "fileReplacements": [
       {
         "replace": "projects/main-app/src/environments/environment.ts",
         "with": "projects/main-app/src/environments/environment.prod.ts"
       }
     ]
   }
 }
}


Intégration de service:



"serve": {
 ...
 "configurations": {
   â€¦ //  
   "cypress": {
     "browserTarget": "main-app:build:cypress"
   }
 }
}


Du principal: pour la configuration de cyprès, nous spécifions l'assemblage aot et remplaçons les fichiers par l'environnement - cela est nécessaire pour créer un assemblage de type prod pendant les tests.



Donc, avec index.html trié, il reste à élever l'application, à attendre que la construction se termine et à exécuter des tests dessus. Pour ce faire, utilisez la bibliothèque start-server-and-test et écrivez des scripts basés sur celle-ci:



 "main-app:cy:run": "cypress run",
 "main-app:cy:open": "cypress open",
 "main-app:integrations": "start-server-and-test main-app:serve:cypress http://localhost:8808/app/user/ main-app:cy:run",
 "main-app:integrations:open": "start-server-and-test main-app:serve:cypress http://localhost:8808/app/user/ main-app:cy:open"


Comme vous pouvez le voir, il existe deux types de scripts: ouvrir et exécuter. Le mode Open ouvre l'interface graphique de Cypress lui-même, où vous pouvez basculer entre les tests et utiliser le voyage dans le temps. Le mode d'exécution est juste une exécution de test et le résultat final de cette exécution, idéal pour une exécution en CI.



Sur la base des résultats du travail effectué, nous avons pu obtenir un cadre de départ pour écrire le premier test et l'exécuter dans CI.



Monorepository



L'approche décrite présente un problème très notable: s'il y a deux applications ou plus dans le référentiel, l'approche à un dossier n'est pas viable. Et c'est arrivé avec nous. Mais c'est arrivé d'une manière assez intéressante. Au moment de l'introduction de Cypress, nous passions à NX, et ce beau prêt à l'emploi permet de travailler avec Cypress. Quel est le principe du travail:



  1. Vous avez une application comme main-app, à côté d'elle l'application principale-app-e2e est créée.

  2. Renommez main-app-e2e en main-app-integrations - vous ĂŞtes incroyable.



Vous pouvez désormais exécuter des tests d'intégration avec une seule commande: ng e2e main-app-integrations. NX affichera automatiquement l'application principale, attendra une réponse et exécutera les tests.



Malheureusement, ceux qui utilisent actuellement Angular Workspace, mais ça va, j'ai aussi une recette pour vous. Nous utiliserons la structure de fichiers comme dans NX:



  1. Créez un dossier main-app-integrations à côté de votre application.

  2. Créez-y un dossier src et ajoutez-y le contenu du dossier cypress.

  3. N'oubliez pas de transférer cypress.json (il apparaîtra initialement à la racine) dans le dossier main-app-integrations.

  4. Modifiez cypress.json, en spécifiant les chemins vers de nouveaux dossiers avec des tests, des plugins et des commandes auxiliaires (paramètres integrationFolder, pluginsFile et supportFile).

  5. Cypress peut travailler avec des tests dans n'importe quel dossier, le paramètre du

    projet est utilisé pour spécifier le dossier , nous changeons donc la commande de cypress run / open à cypress run / open -–project ./projects/main-app-integrations/src .



La solution pour Angular Workspace est la plus similaire à la solution pour NX, sauf que nous créons le dossier à la main et qu'il ne fait pas partie des projets de votre mono-référentiel. Alternativement, vous pouvez utiliser directement le générateur NX pour Cypress ( un exemple de référentiel sur NX avec Cypress, où vous pouvez voir l'utilisation finale du générateur nx-cypress - attention à angular.json et au projet

cart-e2e et products-e2e).



RĂ©gression visuelle



Après les cinq premiers tests, nous avons commencé à penser aux tests de captures d'écran, car, en fait, il y a toutes les possibilités pour cela. Je dirai à l'avance que le mot "test de capture d'écran" cause beaucoup de douleur au sein de l'équipe, car le chemin pour obtenir des tests stables n'était pas le plus facile. Je décrirai ensuite les principaux problèmes rencontrés et leur solution.



La bibliothèque cypress-image-snapshot a été prise comme solution . L'implémentation n'a pas pris beaucoup de temps, et maintenant après 20 minutes, nous avons reçu la première capture d'écran de notre application avec une taille de 1000 × 600 px. Il y avait beaucoup de joie, car l'intégration et l'utilisation étaient trop simples, et les avantages reçus pouvaient être énormes.



Après avoir généré cinq captures d'écran de référence, nous avons effectué une vérification dans CI, en conséquence - la construction s'est effondrée. Il s'est avéré que les captures d'écran créées à l'aide des commandes open et run sont différentes. La solution était assez simple: prendre des captures d'écran uniquement en mode CI, pour cela, ils ont supprimé les captures d'écran en mode local, par exemple comme ceci:



Cypress.Commands.overwrite(
   'matchImageSnapshot',
   (originalFn, subject, fileName, options) => {
       if (Cypress.env('ci')) {
           return originalFn(subject, fileName, options);
       }

       return subject;
   },
);


Dans cette solution, nous examinons le paramètre env dans Cypress, il peut être défini de différentes manières.



Polices



Localement, les tests ont commencé à passer au redémarrage, nous essayons de les relancer en CI. Le résultat peut être vu ci-dessous:







Il est assez facile de remarquer la différence de polices dans la capture d'écran diff. La capture d'écran de référence a été générée sur macOS et dans CI, les agents ont installé Linux.



Mauvaise décision



Nous avons choisi l'une des polices standard (comme Ubuntu Font), qui donnait un diff minimal pixel par pixel, et avons appliqué cette police aux blocs de texte (créé dans

index.html, qui était uniquement destiné aux tests de cyprès). Ensuite, le diff total à 0,05% et le pixel à 20%. Avec ces paramètres, nous avons passé une semaine - jusqu'à la première fois où il était nécessaire de changer le texte dans le composant. En conséquence, la version est restée verte, bien que nous n'ayons pas mis à jour la capture d'écran. La solution actuelle s'est avérée futile.



Solution correcte



Le problème d'origine était dans des environnements différents, la solution, en principe, se suggère - Docker. Il existe déjà des images de docker prêtes à l'emploi pour Cypress . Il existe différentes variantes des images, ce qui nous intéresse, car Cypress est déjà inclus dans l'image et ne téléchargera pas et ne décompactera pas le binaire Cypress à chaque fois (l'interface graphique de Cypress passe par un fichier binaire , et le téléchargement et le décompression prennent plus de temps que le téléchargement). image docker).

Sur la base de l'image de docker incluse, nous créons notre propre conteneur de docker, pour cela, nous avons créé un fichier d'intégration-tests.Dockerfile avec un contenu similaire:



FROM cypress:included:4.3.0
COPY package.json /app/
COPY package-lock.json app/
WORKDIR /app
RUN npm ci
COPY / /app/
ENTRYPOINT []


Je voudrais noter la remise à zéro de ENTRYPOINT, cela est dû au fait qu'il est défini par défaut dans l'image cyprès / inclus et pointe vers la commande d'exécution cypress, ce qui nous empêche d'utiliser d'autres commandes. Nous avons également divisé notre fichier docker en couches afin que chaque fois que nous redémarrons les tests, nous ne réexécutions pas npm ci.



Ajoutez le fichier .dockerignore (s'il n'existe pas) à la racine du référentiel et dans celui-ci, veillez à spécifier node-modules / et * / node-modules /.



Pour exécuter nos tests dans Docker, nous allons écrire un script bash integration-tests.sh avec le contenu suivant:



docker build -t integrations -f integration-tests.Dockerfile .
docker run --rm -v $PWD/projects/main-app-integrations/src:/app/projects/main-app-integrations/src integrations:latest npm run main-app:integrations


Brève description: Nous construisons nos conteneurs d'intégration Docker-tests.Dockerfile et pointons le volume vers le dossier tests afin que nous puissions obtenir les captures d'écran générées à partir de Docker.



Polices Ă  nouveau



Après avoir résolu le problème décrit dans le chapitre précédent, il y a eu une accalmie dans les builds, mais environ un jour plus tard, nous avons rencontré le problème suivant (gauche et droite - captures d'écran d'un composant prises à des moments différents):







Je pense que les plus attentifs ont remarqué qu'il n'y a pas assez de titre dans le popup ... La raison est très simple: la police n'a pas réussi à se charger, car elle n'était pas connectée via des actifs, mais était sur le CDN.



Mauvaise décision



Nous téléchargeons les polices à partir du CDN, les téléchargeons dans les actifs pour la configuration de cyprès et les

incluons dans notre index.html personnalisé pour les tests d'intégration. Nous avons vécu cette décision pendant une période décente, jusqu'à ce que nous changions la police de l'entreprise. Il n'y avait aucune envie de jouer la même histoire une deuxième fois.



Solution correcte



Il a été décidé de commencer à précharger toutes les polices nécessaires pour le test dans

index.html pour la configuration de cyprès, cela ressemblait à ceci:



<link	
      rel="preload"
      href="...."	
      as="font"	
      type="font/woff2"	
      crossorigin="anonymous"
/>


Le nombre d'échecs de test dus à des polices n'ayant pas eu le temps de se charger a diminué au minimum, mais pas à zéro: de toute façon, parfois la police n'a pas eu le temps de se charger. Une solution de KitchenSink de Cypress elle-même est venue à la rescousse - waitForResource.

Dans notre cas, comme le préchargement des polices était déjà connecté, nous avons simplement redéfini la commande de visite dans Cypress, en conséquence, non seulement elle navigue vers la page, mais attend également le chargement des polices spécifiées. Je voudrais également ajouter que waitForResource résout le problème non seulement des polices, mais aussi de toutes les statiques chargées, telles que les images (à cause d'eux, les captures d'écran sont également cassées et waitForResource a beaucoup aidé). Après avoir appliqué cette solution, il n'y a eu aucun problème avec les polices et les statiques téléchargeables.



Animations



Notre mal de tête est lié aux animations, qui perdurent à ce jour. À un moment donné, des captures d'écran commenceront à apparaître sur lesquelles l'élément est animé, ou une capture d'écran est prise avant le début de l'animation. Ces captures d'écran sont instables et chaque fois qu'elles sont comparées à la référence, il y aura des différences. Alors, quel chemin avons-nous pris pour résoudre le problème de l'animation?



Première solution



La chose la plus simple qui nous soit venue à l'esprit au stade initial: avant de créer une capture d'écran, arrêtez le navigateur pendant un certain temps afin que les animations aient le temps de se terminer. Nous avons parcouru la chaîne 100 ms, 200 ms, 500 ms et en conséquence 1000 ms. Avec le recul, je comprends que cette décision était initialement terrible, mais je voulais juste vous mettre en garde contre la même décision. Pourquoi affreux? Le temps d'animation est différent, les agents en CI peuvent aussi parfois jouer un peu, c'est pourquoi tout temps d'attente pour la stabilisation de la page de temps en temps était différent.



Deuxième solution



Même avec une attente de 1 seconde, la page n'a pas toujours réussi à se stabiliser. Après quelques recherches, nous avons trouvé un outil de Angular - Testability. Le principe est basé sur le suivi de la stabilité de ZoneJS:



Cypress.Commands.add('waitStableState', () => {
   return cy.window().then(window => {
       const [testability]: [Testability] = window.getAllAngularTestabilities();

       return new Cypress.Promise(resolve => {
           testability.whenStable(() => {
               resolve();
           }, 3000);
       });
   });
});


Ainsi, lors de la création de captures d'écran, nous avons appelé deux commandes: cy.wait (1000) et cy.waitStableState ().



Depuis lors, aucune capture d'Ă©cran n'a Ă©tĂ© supprimĂ©e de manière alĂ©atoire, mais comptons ensemble le temps passĂ© sur un navigateur inactif. Supposons que vous ayez 5 captures d'Ă©cran prises dans le test, pour chacune il y a un temps d'attente stable de 1 seconde et un temps alĂ©atoire, supposons 1,5 seconde en moyenne (je n'ai pas mesurĂ© la valeur moyenne en rĂ©alitĂ©, donc je l'ai prise de ma tĂŞte en fonction de mes propres sentiments) . En consĂ©quence, nous passons 12,5 secondes supplĂ©mentaires pour crĂ©er des captures d'Ă©cran dans le test. Disons que vous avez dĂ©jĂ  Ă©crit 20 scĂ©narios de test, oĂą chaque test contient au moins 5 captures d'Ă©cran. Nous obtenons que le trop-payĂ© pour la stabilitĂ© est d'environ 4 minutes avec les 20 tests disponibles. 



Mais même ce n'est pas le plus gros problème. Comme indiqué ci-dessus, lors de l'exécution de tests localement, les captures d'écran ne sont pas poursuivies, mais dans CI, elles sont poursuivies, et en raison des attentes pour chaque capture d'écran, les rappels dans le code ont fonctionné, par exemple, à l'heure de rebond, qui a déjà créé une randomisation dans les tests, car dans CI et localement, ils passé de différentes manières.



Solution actuelle



Commençons par les animations angulaires. Notre framework préféré lors de l'animation sur l'élément DOM bloque la classe ng-animating. C'était la clé de notre solution, car maintenant nous devons nous assurer qu'il n'y a pas de classe d'animation sur l'élément maintenant. En conséquence, il est devenu cette fonction:



export function waitAnimation(element: Chainable<JQuery>): Chainable<JQuery> {
   return element.should('be.visible').should('not.have.class', 'ng-animating');
}


Cela ne semble rien de compliqué, mais c'est cela qui a formé la base de nos décisions. Ce à quoi vous voulez faire attention dans cette approche: lorsque vous faites une capture d'écran, vous devez comprendre l'animation de quel élément peut rendre votre capture d'écran instable, et avant de créer une capture d'écran, ajoutez une assertion qui vérifiera que l'élément ne s'anime pas. Mais les animations peuvent également être en CSS. Comme le dit Cypress lui-même, toute assertion sur un élément attend que l'animation se termine dessus - plus de détails ici et ici . Autrement dit, l'essence de l'approche est la suivante: nous avons un élément animé, y ajoutons une assertion - should ('be.visible') / should ('not.be.visible')- et Cypress lui-même attendra la fin de l'animation sur l'élément (peut-être, en passant, la solution avec ng-animating n'est pas nécessaire et seules les vérifications Cypress sont suffisantes, mais pour l'instant nous utilisons l'utilitaire waitAnimation).



Comme indiquĂ© dans la documentation elle-mĂŞme, Cypress vĂ©rifie la position d'un Ă©lĂ©ment sur la page, mais toutes les animations ne concernent pas les changements de position, il existe Ă©galement des animations fadeIn / fadeOut. Dans ces cas, le principe de la solution est le mĂŞme: on vĂ©rifie que l'Ă©lĂ©ment est visible / invisible sur la page. 



Lors du passage de cy.wait (1000) + cy.waitStableState () Ă  waitAnimation et Cypress Assertion, nous avons dĂ» passer ~ 2 heures pour stabiliser les anciennes captures d'Ă©cran, mais en consĂ©quence, nous avons obtenu + 20-30 secondes au lieu de +4 minutes pour le temps d'exĂ©cution du test ... Pour le moment, nous abordons attentivement la revue des captures d'Ă©cran: nous vĂ©rifions qu'elles n'ont pas Ă©tĂ© effectuĂ©es lors de l'animation des Ă©lĂ©ments DOM et des vĂ©rifications ont Ă©tĂ© ajoutĂ©es dans le test d'attente de l'animation. Par exemple, nous ajoutons souvent un affichage «squelette» sur la page jusqu'Ă  ce que les donnĂ©es soient chargĂ©es. En consĂ©quence, l'examen reçoit immĂ©diatement une exigence selon laquelle lors de la crĂ©ation de captures d'Ă©cran, un squelette ne doit pas ĂŞtre prĂ©sent dans le DOM, car il contient une animation de fondu. 



Il n'y a qu'un seul problème avec cette approche: il n'est pas toujours possible de tout prévoir lors de la création d'une capture d'écran, et cela peut encore tomber dans CI. Il n'y a qu'une seule façon de résoudre ce problème: allez immédiatement modifier la création d'une telle capture d'écran, vous ne pouvez pas reporter, sinon elle s'accumulera comme une boule de neige et, finalement, vous désactiverez simplement les tests d'intégration.



Taille de la capture d'Ă©cran



Vous avez peut-être remarqué une fonctionnalité intéressante: la résolution de capture d'écran par défaut est de 1000 × 600 px. Malheureusement, il y a un problème avec la taille de la fenêtre du navigateur lors du démarrage dans Docker: même si vous modifiez la taille de la fenêtre via Cypress, cela n'aidera pas. Nous avons trouvé une solution pour le navigateur Chrome (pour Electron, il n'était pas possible de trouver rapidement une solution fonctionnelle, mais nous n'avons pas lancé la solution proposée dans ce numéro ). Vous devez d'abord modifier le navigateur pour exécuter des tests sur Chrome:



  1. Ce n’est pas pour NX que nous utilisons l’argument --browser chrome lors de l’exécution de la commande cypress open / run, et pour la commande run, spécifiez le paramètre --headless.

  2. Pour NX, dans la configuration du projet dans angular.json avec tests, nous spécifions le paramètre browser: chrome, et pour la configuration qui s'exécutera en CI, nous spécifions headless: true.



Maintenant, nous apportons les modifications dans les plugins et obtenons des captures d'Ă©cran d'une taille de 1440 Ă— 900 px:



module.exports = (on, config) => {
   on('before:browser:launch', (browser, launchOptions) => {
       if (browser.name === 'chrome' && browser.isHeadless) {
           launchOptions.args.push('--disable-dev-shm-usage');
           launchOptions.args.push('--window-size=1440,1200');

           return launchOptions;
       }

       return launchOptions;
   });
};


Rendez-vous



Tout est simple ici: si quelque part la date associée au courant est affichée, la capture d'écran prise aujourd'hui tombera demain. Fixim est simple:



cy.clock(new Date(2025, 11, 22, 0).getTime(), ['Date']);


Maintenant, les minuteries. Nous n'avons pas pris la peine d'utiliser l'option d'occultation lors de la création de captures d'écran, par exemple:



cy.matchImageSnapshot('salary_signing-several-payments', {
   blackout: ['.timer'],
});


Tests feuilletés



En utilisant les recommandations ci-dessus, vous pouvez atteindre une stabilité de test maximale, mais pas à 100%, car les tests sont affectés non seulement par votre code, mais également par l'environnement dans lequel ils sont exécutés.



Par conséquent, un certain pourcentage de tests chutera occasionnellement, par exemple, en raison d'une baisse des performances des agents dans l'IC. Tout d'abord, nous stabilisons le test de notre côté: nous ajoutons les assertions nécessaires avant de prendre des captures d'écran, mais pour la période de réparation de ces tests, vous pouvez réessayer les tests ayant échoué en utilisant cypress-plugin-retries.



Nous pompons CI



Dans les chapitres précédents, nous avons appris à exécuter des tests avec une seule équipe et appris à travailler avec les tests de capture d'écran. Vous pouvez maintenant regarder dans la direction de l'optimisation CI. Notre build fonctionnera certainement:



  1. Équipe NPM CI
  2. Relever l'application en mode aot.
  3. Exécutez des tests d'intégration.


Examinons les premier et deuxième points et comprenons que des étapes similaires sont effectuées dans votre autre build dans CI - la build avec l'assembly d'application.

La principale différence n'est pas d'exécuter ng serve, mais ng build. Ainsi, si nous pouvons obtenir l'application déjà construite dans la build avec des tests d'intégration et augmenter le serveur avec, nous pouvons réduire le temps d'exécution de la build avec des tests.



Pourquoi en avons-nous besoin? C'est juste que notre application est vaste et Ă©panouie

Le démarrage de npm ci + npm en mode aot sur l'agent dans CI a pris environ 15 minutes, ce qui en principe exigeait beaucoup d'efforts de la part de l'agent, et des tests d'intégration ont également été exécutés en plus de cela. Supposons que vous ayez déjà écrit plus de 20 tests et que le 19e test, votre navigateur se bloque, dans lequel des tests sont exécutés, en raison de la forte charge sur l'agent. Comme vous le savez, le redémarrage de la build attend à nouveau que les dépendances soient installées et que l'application démarre.



De plus, je ne parlerai que des scripts côté application. Vous devrez résoudre vous-même le problème du transfert d'artefacts entre les tâches dans CI. Nous garderons donc à l'esprit que la nouvelle version avec des tests d'intégration aura accès à l'application assemblée à partir de la tâche de la construction de votre application.



Serveur avec statique



Nous avons besoin d'un remplaçant pour ng serve pour élever le serveur avec notre application. Il existe de nombreuses options, je vais commencer par notre première - angular-http-server . Il n'y a rien de compliqué dans sa configuration: nous installons la dépendance, indiquons dans quel dossier se trouvent nos statiques, indiquons sur quel port lancer l'application, et sommes satisfaits.



Cette solution nous a suffi pendant les 20 minutes entières, puis nous avons réalisé que nous voulions envoyer certaines requêtes au circuit de test. Échec de la connexion du proxy pour le serveur http angulaire. La solution finale était de mettre à niveau le serveur vers Express . Pour résoudre le problème, nous avons utilisé le proxy express et express-http lui-même. Nous distribuerons notre statique en utilisant

express.static, par conséquent, nous obtenons un script similaire à celui-ci:



const express = require('express');
const appStaticPathFolder = './dist';
const appBaseHref = './my/app';
const port = 4200;
const app = express();

app.use((req, res, next) => {
   const accept = req
       .accepts()
       .join()
       .replace('*/*', '');

   if (accept.includes('text/html')) {
       req.url = baseHref;
   }

   next();
});
app.use(appBaseHref, express.static(appStaticPathFolder));
app.listen(port);


Le point intéressant ici est qu'avant d'écouter l'itinéraire sur la baseHref de l'application, nous traitons également toutes les requêtes et recherchons une requête pour index.html. Cela est fait pour les cas où les tests vont sur une page d'application dont le chemin est différent de baseHref. Si vous ne faites pas cette astuce, lorsque vous accédez à n'importe quelle page de votre application, à l'exception de la page principale, vous recevrez une erreur 404. Ajoutons maintenant une pincée de proxy:



const proxy = require('express-http-proxy');

app.use(
   '/common',
   proxy('https://qa-stand.ru', {
       proxyReqPathResolver: req => '/common' + req.url,
   }),
);


Regardons de plus près ce qui se passe. Il y a des constantes:



  1. appStaticForlderPath - le dossier dans lequel se trouve la statique de votre application. 
  2. appBaseHref - votre application peut avoir un baseHref, sinon, vous pouvez spécifier '/'.


Nous mandatons toutes les demandes commençant par / common, et lors de la procuration, nous enregistrons le même chemin d'accès que la demande en utilisant le paramètre proxyReqPathResolver. Si vous ne l'utilisez pas, toutes les demandes iront simplement à https://qa-stand.ru.



Index de personnalisation.html



Nous devions résoudre le problème avec le fichier index.html personnalisé, que nous utilisions lorsque nous servions des applications en mode Cypress. Écrivons un script simple dans node.js. Nous avions index.modern.html comme paramètres initiaux, nous devions le transformer en index.html et supprimer les scripts inutiles à partir de là:



const fs = require('fs');
const appStaticPathFolder = './dist';

fs.copyFileSync(appStaticPathFolder + '/index.modern.html', appStaticPathFolder + '/index.html');

fs.readFile(appStaticPathFolder + '/index.html', 'utf-8', (err, data) => {
   const newValue = data
       .replace(
           '<script type="text/javascript" src="/auth.js"></script>',
           '',
       )
       .replace(
           '<script type="text/javascript" src="/analytics.js"></script>',
           '',
       );

   fs.writeFileSync(appStaticPathFolder + '/index.html', newValue, 'utf-8');
});


Les scripts



Je ne voulais vraiment pas refaire npm ci de toutes les dépendances pour exécuter des tests dans CI (après tout, cela avait déjà été fait dans la tâche avec la construction de l'application), donc l'idée est venue de créer un dossier séparé pour tous ces scripts avec notre propre package.json. Nommons le dossier, par exemple, scripts-tests d'intégration et déposons-y trois fichiers: server.js, create-index.js, package.json. Les deux premiers fichiers ont été décrits ci-dessus, analysons maintenant le contenu de package.json:



{
 "name": "cypress-tests",
 "version": "0.0.0",
 "private": true,
 "scripts": {
   "create-index": "node ./create-index.js",
   "main-app:serve": "node ./server.js",
   "main-app:cy:run": "cypress run --project ./projects/main-app-integrations ",
   "main-app:integrations": "npm run create-index && start-server-and-test main-app:serve http://localhost:4200/my/app/ main-app:cy:run"
 },
 "devDependencies": {
   "@cypress/webpack-preprocessor": "4.1.0",
   "@types/express": "4.17.2",
   "@types/mocha": "5.2.7",
   "@types/node": "8.9.5",
   "cypress": "4.1.0",
   "cypress-image-snapshot": "3.1.1",
   "express": "4.17.1",
   "express-http-proxy": "^1.6.0",
   "start-server-and-test": "1.10.8",
   "ts-loader": "6.2.1",
   "typescript": "3.8.3",
   "webpack": "4.41.6"
 }
}


Dans package.json, il n'y a que des dépendances nécessaires pour exécuter des tests d'intégration ( avec prise en charge de typescript et des tests de capture d'écran) et des scripts pour démarrer le serveur, créer index.html et le bien connu du chapitre sur le lancement des tests d'intégration dans Angular Workspace start-server-and-test .



Fonctionnement



Nous enveloppons l'exécution des tests d'intégration dans un nouveau Dockerfile - integration-tests-ci.Dockerfile :



FROM cypress/included:4.3.0
COPY integration-tests-scripts /app/
WORKDIR /app
RUN npm ci
COPY projects/main-app-integrations /app/projects/main-app-integrations
COPY dist /app/dist
COPY tsconfig.json /app/
ENTRYPOINT []


La ligne du bas est simple: copiez et développez le dossier Integration-tests-scripts à la racine de l'application et copiez tout ce qui est nécessaire pour exécuter les tests (cet ensemble peut différer pour vous). Les principales différences par rapport au fichier précédent sont que nous ne copions pas l'ensemble de l'application dans le conteneur docker, juste l'optimisation minimale du temps d'exécution du test dans CI.



Créez un fichier integration-tests-ci.sh avec le contenu suivant:



docker build -t integrations -f integration-tests-ci.Dockerfile .
docker run --rm -v $PWD/projects/main-app-integrations/src:/app/projects/main-app-integrations/src integrations:latest npm run main-app:integrations


Lorsque la commande avec tests est exécutée, package.json du dossier integration-tests-scripts deviendra la racine et la commande main-app: integrations y sera lancée. En conséquence, puisque ce dossier se développera à la racine, les chemins d'accès au dossier avec les statistiques de votre application doivent être indiqués avec l'idée que tout sera lancé à partir de la racine, et non à partir du dossier intégration-tests-scripts.



Je veux aussi faire une petite remarque: le script bash final pour exécuter les tests d'intégration au fur et à mesure de son évolution, je l'ai appelé différemment. Vous n'avez pas besoin de le faire, cela a été fait uniquement pour la commodité de la lecture de cet article. Vous devriez toujours avoir un fichier, par exemple integration-tests.sh, que vous développez déjà. Si vous avez plusieurs applications dans le référentiel et que leurs méthodes de préparation sont différentes, vous pouvez résoudre soit avec des variables dans bashou des fichiers différents pour chaque application - en fonction de vos besoins.



Résumé



Il y avait beaucoup d'informations - je pense que maintenant cela vaut la peine de résumer sur la base de ce qui a été écrit ci-dessus.

Préparation des outils pour l'écriture locale et exécution de tests avec une pincée de tests de capture d'écran:



  1. Ajoutez une dépendance pour Cypress.
  2. Préparation d'un dossier avec des tests:

    1. Application unique angulaire - laissez tout dans le dossier cyprès.
    2. Angular Workspace - créez un dossier nom-intégrations d'application à côté de l'application contre laquelle les tests seront exécutés, et déplacez tout du dossier cyprès vers celui-ci.
    3. NX - renommez le projet appname-e2e en appname-integrations.


  3. cypress- — build- Cypress, aot, index.html, environment prod- serve- Cypress ( , - prod-, ).
  4. :

    1. Angular Single Application — serve- cypress- , start-server-and-test.
    2. Angular Workspace — Angular Single Application, cypress run/open.
    3. NX — ng e2e.


  5. -:

    1. cypress-image-snapshot.
    2. CI.
    3. Lors des tests, nous ne prenons pas de captures d'écran au hasard. Si la capture d'écran est précédée d'une animation, assurez-vous de l'attendre - par exemple, ajoutez Cypress Assertion à l'élément animé.
    4. Nous nous moquons de la date via cy.clock ou utilisons l'option d'occultation lors de la capture d'Ă©cran.
    5. Nous attendons toutes les statiques chargées lors de l'exécution via la commande cy.waitForResource personnalisée (images, polices, etc.).


  6. Enveloppez le tout dans Docker:

    1. Préparation du Dockerfile.
    2. Créez un fichier bash.




 ExĂ©cutez les tests par-dessus l'application assemblĂ©e:



  1. Dans CI, nous apprenons à lancer des artefacts de l'application assemblée entre les builds (c'est à vous).
  2. Préparation du dossier Integration-tests-scripts:

    1. Un script pour Ă©lever le serveur de votre application.
    2. Un script pour changer votre index.html (si vous ĂŞtes satisfait de l'index.html initial, vous pouvez l'ignorer).
    3. Ajoutez au dossier package.json avec les scripts et dépendances nécessaires.
    4. Préparation d'un nouveau Dockerfile.
    5. Créez un fichier bash.


Liens utiles



  1. Angular Workspace + Cypress + CI — Angular Workspace CI , ( typescript).

  2. Cypress — trade-offs.

  3. Start-server-and-test — , .

  4. Cypress-image-snapshot — -.

  5. Cypress recipes — Cypress, .

  6. Flaky Tests — , Google.

  7. Github Action — Cypress GitHub Action, README , — wait-on. docker.




All Articles