Wir schreiben Integrationstests für das Frontend und beschleunigen Releases

Hallo! Mein Name ist Vova, ich bin ein Frontend bei Tinkoff. Unser Team ist verantwortlich für zwei Produkte für juristische Personen. Ich kann in Zahlen über die Größe des Produkts sagen: Eine vollständige Regression jedes Produkts durch zwei Tester dauert drei Tage (ohne den Einfluss externer Faktoren).



Der Zeitrahmen ist bedeutend und muss behandelt werden. Es gibt verschiedene Möglichkeiten zu kämpfen, die wichtigsten sind:



  • Schneiden der Anwendung in kleinere Produkte mit eigenen Release-Zyklen.

  • Abdeckung des Produkts mit Tests gemäß der Testpyramide.



Der letzte Absatz war das Thema meines Artikels.



Bild



Pyramide testen



Wie wir wissen, gibt es in der Testpyramide drei Ebenen: Komponententests, Integrationstests und e2e-Tests. Ich denke, viele sind sowohl mit Einheiten als auch mit e2e vertraut, daher werde ich mich eingehender mit Integrationstests befassen.



Im Rahmen von Integrationstests testen wir den Betrieb der gesamten Anwendung durch Interaktion mit der Benutzeroberfläche. Der Hauptunterschied zu e2e-Tests besteht jedoch darin, dass wir keine echten Anforderungen für die Sicherung stellen. Dies geschieht, um nur das Zusammenspiel aller Systeme an der Front zu überprüfen und die Anzahl der e2e-Tests in Zukunft zu reduzieren.



Zum Schreiben von Integrationstests verwenden wir Cypress. In diesem Artikel werde ich es nicht mit anderen Frameworks vergleichen, ich werde nur sagen, warum wir damit gelandet sind:



  1. Sehr detaillierte Dokumentation.

  2. Einfaches Debuggen von Tests (Cypress hat hierfür eine spezielle GUI mit Zeitreiseschritten im Test erstellt).



Diese Punkte waren für unser Team wichtig, da wir keine Erfahrung mit dem Schreiben von Integrationstests hatten und ein sehr einfacher Start erforderlich war. In diesem Artikel möchte ich über den Weg sprechen, den wir gegangen sind, über die Unebenheiten, die wir gefüllt haben, und Rezepte für die Implementierung teilen.



Der Anfang des Weges



Am Anfang habe ich Angular Workspace mit einer Anwendung verwendet, um den Code zu organisieren. Nach der Installation des Cypress-Pakets wurde im Stammverzeichnis der Anwendung ein Cypress-Ordner mit Konfiguration und Tests angezeigt. Bei dieser Option haben wir aufgehört. Beim Versuch, ein Skript in package.json vorzubereiten, das zum Ausführen der Anwendung und zum Ausführen von Tests erforderlich ist, sind die folgenden Probleme aufgetreten:



  1. Index.html enthält einige Skripte, die in Integrationstests nicht benötigt werden.

  2. Um die Integrationstests auszuführen, mussten Sie sicherstellen, dass der Server mit der Anwendung ausgeführt wurde.



Das Problem mit index.html wurde durch eine separate Build-Konfiguration - nennen wir es sypress - gelöst, in der wir eine benutzerdefinierte index.html angegeben haben. Wie implementiere ich das? Suchen Sie die Konfiguration Ihrer Anwendung in angle.json, öffnen Sie den Build-Abschnitt, fügen Sie dort eine separate Konfiguration für Cypress hinzu, und vergessen Sie nicht, diese Konfiguration für den Serve-Modus anzugeben.



Beispielkonfiguration für Build:



"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"
       }
     ]
   }
 }
}


Integration dienen:



"serve": {
 ...
 "configurations": {
   … //  
   "cypress": {
     "browserTarget": "main-app:build:cypress"
   }
 }
}


Aus der Hauptsache: Für die Cypress-Konfiguration geben wir die aot-Assembly an und ersetzen die Dateien durch die Umgebung. Dies ist erforderlich, um während des Tests eine prod-ähnliche Assembly zu erstellen.



Also haben wir die index.html herausgefunden. Es bleibt, die Anwendungen zu erhöhen, auf den Abschluss des Builds zu warten und darüber hinaus Tests auszuführen. Verwenden Sie dazu die Start-Server-and-Test- Bibliothek und schreiben Sie darauf basierende Skripte:



 "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"


Wie Sie sehen können, gibt es zwei Arten von Skripten: Öffnen und Ausführen. Der Öffnungsmodus öffnet die GUI von Cypress selbst, wo Sie zwischen Tests wechseln und Zeitreisen verwenden können. Der Ausführungsmodus ist nur ein Testlauf und das Endergebnis dieses Laufs, ideal für die Ausführung in CI.



Basierend auf den Ergebnissen der geleisteten Arbeit konnten wir einen Startrahmen für das Schreiben des ersten Tests und dessen Ausführung in CI erhalten.



Monorepository



Der beschriebene Ansatz weist ein sehr auffälliges Problem auf: Wenn das Repository zwei oder mehr Anwendungen enthält, ist der Ansatz mit einem Ordner nicht realisierbar. Und so passierte es bei uns. Aber es passierte auf ziemlich interessante Weise. Zum Zeitpunkt der Einführung von Cypress waren wir auf NX umgestiegen, und dieses ansehnliche Produkt ermöglicht die Zusammenarbeit mit Cypress. Was ist das Prinzip der Arbeit darin:



  1. Sie haben eine Anwendung wie die Haupt-App, daneben wird die Haupt-App-e2e-Anwendung erstellt.

  2. Benennen Sie main-app-e2e in main-app-integrations um - Sie sind unglaublich.



Jetzt können Sie Integrationstests mit einem Befehl ausführen - ng e2e main-app-integrations. NX ruft automatisch die Haupt-App auf, wartet auf eine Antwort und führt die Tests aus.



Leider sind diejenigen, die derzeit Angular Workspace verwenden, am Rande geblieben, aber das ist in Ordnung, ich habe auch ein Rezept für Sie. Wir werden die Dateistruktur wie in NX verwenden:



  1. Erstellen Sie einen Hauptordner für die App-Integration neben Ihrer Anwendung.

  2. Wir erstellen den src-Ordner darin und legen den Inhalt des Cypress-Ordners darin ab.

  3. Vergessen Sie nicht, cypress.json (es wird zunächst im Stammverzeichnis angezeigt) in den Ordner für die Haupt-App-Integration zu übertragen.

  4. Wir korrigieren cypress.json und geben die Pfade zu neuen Ordnern mit Tests, Plugins und Hilfsbefehlen an (IntegrationFolder-, PluginsFile- und SupportFile-Parameter).

  5. Cypress kann mit Tests in beliebigen Ordnern arbeiten. Der

    Projektparameter wird verwendet, um den Ordner anzugeben . Daher ändern wir den Befehl von cypress run / open in cypress run / open -–project ./projects/main-app-integrations/src .



Die Lösung für Angular Workspace ist der Lösung für NX so ähnlich wie möglich, außer dass der Ordner von Hand erstellt wird und nicht zu den Projekten in Ihrem Monorepository gehört. Alternativ können Sie den NX-Builder direkt für Cypress verwenden ( ein Beispiel für ein Repository unter NX mit Cypress. Dort können Sie die endgültige Verwendung des Builders nx-cypress sehen - beachten Sie angle.json und das

Projekt cart-e2e und products-e2e).



Visuelle Regression



Nach den ersten fünf Tests haben wir angefangen, über Screenshot-Tests nachzudenken, da es tatsächlich alle Möglichkeiten dafür gibt. Ich werde im Voraus sagen, dass das Wort "Screenshot-Test" im Team große Schmerzen verursacht, da der Weg zu stabilen Tests nicht der einfachste war. Als nächstes werde ich die Hauptprobleme, auf die wir gestoßen sind, und ihre Lösung beschreiben.



Die Cypress-Image-Snapshot- Bibliothek wurde als Lösung genommen . Die Implementierung dauerte nicht lange und nach 20 Minuten erhielten wir den ersten Screenshot unserer Anwendung mit einer Größe von 1000 × 600 px. Es gab viel Freude, weil die Integration und Verwendung zu einfach waren und die erzielten Vorteile enorm sein konnten.



Nachdem wir fünf Referenz-Screenshots erstellt hatten, führten wir eine Überprüfung in CI durch - der Build fiel auseinander. Es stellte sich heraus, dass die mit den Befehlen open und run aufgenommenen Screenshots unterschiedlich sind. Die Lösung war ganz einfach: Screenshots nur im CI-Modus machen, dafür wurden Screenshots im lokalen Modus entfernt, zum Beispiel wie folgt:



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

       return subject;
   },
);


In dieser Lösung betrachten wir den env-Parameter in Cypress, der auf verschiedene Arten eingestellt werden kann.



Schriftarten



Vor Ort begannen die Tests nach dem Neustart zu bestehen. Wir versuchen, sie erneut in CI auszuführen. Das Ergebnis ist unten zu sehen:







Es ist ziemlich leicht, den Unterschied in den Schriftarten im Diff-Screenshot zu bemerken. Unter macOS wurde ein Referenz-Screenshot erstellt und auf den Agenten in CI Linux installiert.



Fehlentscheidung



Wir haben eine der Standardschriftarten (wie Ubuntu Font) ausgewählt, die einen minimalen Unterschied pro Pixel ergab, und diese Schriftart für Textblöcke angewendet (erstellt in

index.html, die nur für Zypressen-Tests vorgesehen war). Dann haben wir den Gesamtdiff auf 0,05% und den Diff pro Pixel auf 20% erhöht. Mit diesen Parametern haben wir eine Woche verbracht - bis zum ersten Mal, als es notwendig war, den Text in der Komponente zu ändern. Infolgedessen blieb der Build grün, obwohl wir den Screenshot nicht aktualisiert haben. Die derzeitige Lösung hat sich als zwecklos erwiesen.



Richtige Lösung



Das ursprüngliche Problem war in verschiedenen Umgebungen, die Lösung bietet sich im Prinzip an - Docker. Es gibt bereits vorgefertigte Docker-Images für Cypress . Es gibt verschiedene Bildvarianten, an denen wir interessiert sind, da Cypress bereits im Bild enthalten ist und die Cypress-Binärdatei nicht jedes Mal heruntergeladen und wieder entpackt wird (die Cypress-GUI wird über eine Binärdatei gestartet , und das Herunterladen und Entpacken dauert länger als das Herunterladen Docker-Bild).

Basierend auf dem enthaltenen Docker-Image erstellen wir unseren eigenen Docker-Container. Dazu haben wir eine Integrationstests erstellt. Docker-Datei mit ähnlichem Inhalt:



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


Ich möchte die Nullstellung von ENTRYPOINT beachten. Dies liegt an der Tatsache, dass sie standardmäßig im cypress / include-Image festgelegt ist und auf den Befehl cypress run verweist, der uns daran hindert, andere Befehle zu verwenden. Wir teilen unsere Docker-Datei auch in Ebenen auf, damit Sie bei jedem Neustart der Tests npm ci nicht erneut ausführen müssen.



Fügen Sie die .dockerignore-Datei (falls nicht vorhanden) zum Stammverzeichnis des Repositorys hinzu und geben Sie darin die Knotenmodule / und * / Knotenmodule / an.



Um unsere Tests in Docker auszuführen, schreiben wir ein Bash-Skript integrations-tests.sh mit folgendem Inhalt:



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


Kurzbeschreibung: Wir erstellen unseren Docker-Container für die Integrationstests. Docker-Datei und zeigen das Volume auf den Testordner, damit wir die generierten Screenshots von Docker erhalten können.



Wieder Schriften



Nachdem wir das im vorherigen Kapitel beschriebene Problem gelöst hatten, gab es eine Pause in den Builds, aber ungefähr einen Tag später hatten wir das folgende Problem (links und rechts - Screenshots derselben Komponente, die zu unterschiedlichen Zeiten aufgenommen wurden):







Ich denke, die aufmerksamsten bemerkten, dass das Popup nicht genügend Titel enthielt . Der Grund ist sehr einfach: Die Schriftart konnte nicht geladen werden, da sie nicht über Assets verbunden war, sondern sich auf dem CDN befand.



Fehlentscheidung



Laden Sie Schriftarten von CDN herunter, legen Sie sie für die Cypress-Konfiguration in Assets ab und

verbinden Sie sie in unserer benutzerdefinierten index.html für Integrationstests. Mit dieser Entscheidung lebten wir eine anständige Zeit, bis wir die Unternehmensschriftart änderten. Es bestand kein Wunsch, dieselbe Geschichte ein zweites Mal zu spielen.



Richtige Lösung



Es wurde beschlossen, alle für den Test erforderlichen Schriftarten in

index.html für die Cypress-Konfiguration vorzuladen. Es sah ungefähr so ​​aus:



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


Die Anzahl der Testabstürze aufgrund von Schriftarten, für die keine Zeit zum Laden vorhanden war, verringerte sich auf ein Minimum, jedoch nicht auf Null. Trotzdem hatte die Schrift manchmal keine Zeit zum Laden. Die Lösung von KitchenSink of Cypress selbst kam zur Rettung - waitForResource.

In unserem Fall haben wir den Befehl visit in Cypress einfach neu definiert, da das Vorladen der Schriftarten bereits verbunden war. Dadurch wird nicht nur zur Seite navigiert, sondern es wird auch darauf gewartet, dass die angegebenen Schriftarten geladen werden. Ich möchte auch hinzufügen, dass waitForResource das Problem nicht nur von Schriftarten, sondern auch von geladenen Statiken wie Bildern löst (aufgrund dessen sind auch Screenshots kaputt gegangen und waitForResource hat sehr geholfen). Nach dem Anwenden dieser Lösung gab es keine Probleme mit Schriftarten und herunterladbaren Statiken.



Animationen



Mit Animationen sind unsere Kopfschmerzen verbunden, die bis heute bestehen bleiben. Irgendwann werden Screenshots des Elements angezeigt, oder es wurde ein Screenshot erstellt, bevor die Animation gestartet wird. Solche Screenshots sind instabil und jedes Mal, wenn sie mit der Referenz verglichen werden, gibt es Unterschiede. Welchen Weg haben wir bei der Lösung des Animationsproblems eingeschlagen?



Erste Entscheidung



Das Einfachste, was uns in der Anfangsphase eingefallen ist: Halten Sie den Browser vor dem Erstellen eines Screenshots für eine bestimmte Zeit an, damit die Animationen abgeschlossen werden können. Wir gingen entlang der Kette 100ms, 200ms, 500ms und als Ergebnis 1000ms. Rückblickend verstehe ich, dass diese Entscheidung anfangs schrecklich war, aber ich wollte Sie nur vor derselben Entscheidung warnen. Warum schrecklich? Die Animationszeiten sind unterschiedlich, Agenten in CI können manchmal auch langweilig sein, weshalb die Wartezeit für die Seitenstabilisierung von Zeit zu Zeit unterschiedlich war.



Zweite Lösung



Selbst mit einer Wartezeit von 1 Sekunde gelang es der Seite nicht immer, stabil zu werden. Nach einer kurzen Pause fanden wir das Tool bei Angular - Testability. Das Prinzip basiert auf der Verfolgung der Stabilität von ZoneJS:



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

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


Daher haben wir beim Erstellen von Screenshots zwei Befehle aufgerufen: cy.wait (1000) und cy.waitStableState ().



Seitdem gab es keinen einzigen zufällig abgelegten Screenshot, aber lassen Sie uns gemeinsam berechnen, wie viel Zeit im Leerlauf des Browsers verbracht wurde. Angenommen, Sie haben 5 Screenshots in Ihrem Test, für jede gibt es eine stabile Wartezeit von 1 Sekunde und eine zufällige Zeit, sagen wir durchschnittlich 1,5 Sekunden (ich habe den Durchschnittswert in der Realität nicht gemessen, also habe ich ihn nach meinen eigenen Gefühlen aus meinem Kopf genommen). . Infolgedessen verbringen wir zusätzliche 12,5 Sekunden damit, Screenshots im Test zu erstellen. Stellen Sie sich vor, Sie haben bereits 20 Testskripte geschrieben, wobei jeder Test mindestens 5 Screenshots enthält. Wir erhalten, dass die Überzahlung für Stabilität bei den verfügbaren 20 Tests ~ 4 Minuten beträgt. 



Dies ist jedoch nicht einmal das größte Problem. Wie oben erläutert, werden Screenshots beim lokalen Ausführen von Tests nicht verfolgt, sondern in CI. Aufgrund der Erwartungen für jeden Screenshot funktionierten die Rückrufe im Code beispielsweise zum Zeitpunkt des Entprellens, wodurch bereits eine Randomisierung in den Tests erstellt wurde, da sie in CI und lokal ausgeführt wurden auf verschiedene Weise bestanden.



Aktuelle Lösung



Beginnen wir mit Angular-Animationen. Unser Lieblingsframework hängt die ng-animierende Klasse während der Animation an das DOM-Element. Dies war der Schlüssel zu unserer Entscheidung, denn jetzt müssen wir sicherstellen, dass das Element jetzt keine Animationsklasse hat. Infolgedessen wurde daraus folgende Funktion:



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


Es scheint nichts Kompliziertes zu sein, aber das war die Grundlage unserer Entscheidungen. Worauf Sie bei diesem Ansatz achten möchten: Wenn Sie einen Screenshot erstellen, müssen Sie die Animation verstehen, durch welches Element Ihr Screenshot instabil werden kann, und vor dem Erstellen des Screenshots eine Zusicherung hinzufügen, um sicherzustellen, dass das Element nicht animiert wird. Animationen können aber auch in CSS sein. Wie Cypress selbst sagt, wartet jede Behauptung zu einem Element darauf, dass die Animation darauf endet - mehr Details hier und hier . Das heißt, das Wesentliche des Ansatzes ist wie folgt: Wir haben ein animiertes Element, fügen eine Behauptung hinzu - sollte ('sichtbar sein') / sollte ('nicht sichtbar sein')- und Cypress selbst wartet darauf, dass die Animation für das Element beendet wird (möglicherweise ist übrigens keine Lösung mit ng-animierung erforderlich und nur Cypress-Überprüfungen sind ausreichend, aber im Moment verwenden wir das Dienstprogramm waitAnimation).



Wie in der Dokumentation selbst angegeben, prüft Cypress, ob sich die Position eines Elements auf einer Seite geändert hat, aber nicht alle Animationen zum Ändern einer Position. Es gibt auch FadeIn / FadeOut-Animationen. In diesen Fällen ist das Lösungsprinzip dasselbe: Wir prüfen, ob das Element auf der Seite sichtbar / unsichtbar ist. 



Beim Wechsel von cy.wait (1000) + cy.waitStableState () zu waitAnimation und Cypress Assertion mussten wir ~ 2 Stunden für die Stabilisierung alter Screenshots aufwenden, aber als Ergebnis erhielten wir + 20-30 Sekunden anstelle von +4 Minuten für die Testausführungszeit . Im Moment nähern wir uns sorgfältig der Überprüfung der Screenshots: Wir überprüfen, ob sie während der Animation der DOM-Elemente nicht ausgeführt wurden, und im Test wurden Überprüfungen hinzugefügt, um auf die Animation zu warten. Beispielsweise fügen wir häufig die Anzeige von "Skeletten" auf der Seite hinzu, bis die Daten geladen wurden. Dementsprechend erhält die Überprüfung sofort die Anforderung, dass beim Erstellen von Screenshots kein Skelett im DOM vorhanden sein darf, da es eine Überblendungsanimation enthält. 



Das Problem bei diesem Ansatz ist eines: Es ist nicht immer möglich, beim Erstellen eines Screenshots alles vorherzusehen, und es kann immer noch in CI fallen. Es gibt nur einen Weg, um damit umzugehen: Bearbeiten Sie sofort die Erstellung eines solchen Screenshots, Sie können ihn nicht verschieben, da er sich sonst wie ein Schneeball ansammelt und Sie die Integrationstests letztendlich einfach ausschalten.



Screenshotgröße



Möglicherweise haben Sie eine interessante Funktion bemerkt: Die Standardauflösung für Screenshots beträgt 1000 × 600 px. Leider gibt es ein Problem mit der Größe des Browserfensters, wenn es in Docker ausgeführt wird: Selbst wenn Sie die Größe des Ansichtsfensters über Cypress ändern, hilft dies nicht. Wir haben eine Lösung für den Chrome-Browser gefunden (für Electron konnten wir nicht schnell eine funktionierende Lösung finden, und wir haben die in dieser Ausgabe vorgeschlagene nicht erhalten). Zunächst müssen Sie den Browser ändern, um Tests in Chrome auszuführen:



  1. Nicht für NX verwenden wir das Argument --browser chrome, wenn wir den Befehl cypress open / run starten, und für den Befehl run geben wir den Parameter --headless an.

  2. Für NX geben wir in der Projektkonfiguration in angular.json mit Tests den Parameter browser: chrome an, und für die Konfiguration, die in CI ausgeführt wird, geben wir headless: true an.



Jetzt bearbeiten wir Plugins und erhalten Screenshots mit einer Größe von 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;
   });
};


Termine



Hier ist alles einfach: Wenn das mit dem aktuellen Datum verknüpfte Datum irgendwo angezeigt wird, fällt der heute aufgenommene Screenshot morgen. Fixim ist einfach:



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


Nun die Timer. Wir haben uns beim Erstellen von Screenshots nicht die Mühe gemacht, die Blackout-Option zu verwenden, zum Beispiel:



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


Flockige Tests



Mit den obigen Empfehlungen können Sie maximale Teststabilität erreichen, jedoch nicht 100%, da Tests nicht nur von Ihrem Code, sondern auch von der Umgebung, in der sie ausgeführt werden, beeinflusst werden.



Infolgedessen sinkt gelegentlich ein bestimmter Prozentsatz der Tests, beispielsweise aufgrund eines Rückgangs der Agentenleistung in CI. Zunächst stabilisieren wir den Test von unserer Seite: Wir fügen die erforderlichen Aussagen hinzu, bevor wir Screenshots machen. Für die Reparatur solcher Tests können Sie die fehlgeschlagenen Tests jedoch mithilfe von Cypress-Plugin-Wiederholungsversuchen wiederholen .



Wir pumpen CI



In den vorherigen Kapiteln haben wir gelernt, wie man Tests mit einem Befehl ausführt und wie man mit Screenshot-Tests arbeitet. Jetzt können Sie in Richtung CI-Optimierung schauen. Unser Build wird definitiv laufen:



  1. Npm ci Befehl.
  2. Erhöhen der Anwendung im aot-Modus.
  3. Führen Sie Integrationstests aus.


Schauen wir uns den ersten und zweiten Punkt an und verstehen, dass ähnliche Schritte in Ihrem anderen Build in CI ausgeführt werden - dem Build mit der Anwendungsassembly.

Der Hauptunterschied besteht nicht darin, zu dienen, sondern zu bauen. Wenn wir also eine bereits zusammengestellte Anwendung in einem Build mit Integrationstests erhalten und den Server damit anheben können, können wir die Ausführungszeit des Builds mit Tests reduzieren.



Warum brauchten wir es? Wir haben nur eine große Anwendung und Ausführung

Der Start von npm ci + npm im aot-Modus auf dem Agenten in CI dauerte ~ 15 Minuten, was im Prinzip viel Aufwand für den Agenten erforderte, und darüber hinaus wurden Integrationstests durchgeführt. Angenommen, Sie haben bereits mehr als 20 Tests geschrieben und beim 19. Test stürzt Ihr Browser ab, bei dem Tests ausgeführt werden, da der Agent stark belastet ist. Wie Sie wissen, wartet der Neustart des Builds erneut darauf, dass Abhängigkeiten installiert und die Anwendung gestartet wird.



Von nun an werde ich nur noch auf anwendungsseitige Skripte eingehen. Sie müssen das Problem der Übertragung von Artefakten zwischen Aufgaben an CI selbst lösen. Beachten Sie daher, dass ein neuer Build mit Integrationstests von der Aufgabe für Ihren Anwendungsbuild aus auf die zusammengestellte Anwendung zugreifen kann.



Statischer Server



Wir benötigen einen Ersatz für ng serve, um den Server mit unserer Anwendung zu erhöhen. Es gibt viele Möglichkeiten, ich beginne mit unserer ersten - Angular-http-Server . Das Einrichten ist nicht schwierig: Wir installieren die Abhängigkeit, geben an, in welchem ​​Ordner sich unsere Statik befindet, geben an, an welchem ​​Port die Anwendung gestartet werden soll, und freuen uns.



Diese Lösung reichte uns bis zu 20 Minuten lang, und dann wurde uns klar, dass wir einige Anfragen an die Testschaltung weiterleiten möchten. Verbindungsproxy für Angular-http-Server fehlgeschlagen. Die endgültige Lösung bestand darin, den Server auf Express zu aktualisieren . Um das Problem zu lösen, wurden Express und Express-http-Proxy selbst verwendet. Wir werden unsere Statik mit

express.static verteilen. Als Ergebnis erhalten wir ein Skript, das diesem ähnlich ist:



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);


Der interessante Punkt hierbei ist, dass wir vor dem Abhören der Route auf der baseHref der Anwendung auch alle Anforderungen verarbeiten und nach einer Anforderung für index.html suchen. Dies erfolgt in Fällen, in denen die Tests auf die Anwendungsseite gehen, deren Pfad sich von baseHref unterscheidet. Wenn Sie diesen Trick nicht ausführen, wird beim Aufrufen einer beliebigen Seite Ihrer Anwendung mit Ausnahme der Hauptseite ein 404-Fehler angezeigt. Fügen Sie nun eine Prise Proxy hinzu:



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

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


Schauen wir uns genauer an, was passiert. Es gibt Konstanten:



  1. appStaticForlderPath - Der Ordner, in dem sich die Statik Ihrer Anwendung befindet. 
  2. appBaseHref - Ihre Anwendung verfügt möglicherweise über eine baseHref. Andernfalls können Sie '/' angeben.


Wir vertreten alle Anforderungen, die mit / common beginnen, als Proxy. Beim Proxy speichern wir denselben Pfad, den die Anforderung mit der Einstellung proxyReqPathResolver hatte. Wenn Sie es nicht verwenden, werden alle Anfragen einfach an https://qa-stand.ru gesendet.



Anpassungsindex.html



Wir mussten das Problem mit der benutzerdefinierten index.html lösen, die wir verwendet haben, um die Anwendung im Cypress-Modus bereitzustellen. Schreiben wir ein einfaches Skript in node.js. Wir hatten index.modern.html als Anfangsparameter, wir mussten es in index.html umwandeln und unnötige Skripte von dort entfernen:



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');
});


Skripte



Ich wollte wirklich nicht noch einmal npm ci aller Abhängigkeiten ausführen, um Tests in CI auszuführen (schließlich wurde dies bereits in der Aufgabe mit dem Anwendungsbuild durchgeführt), daher schien die Idee, einen separaten Ordner für alle diese Skripte mit unserer eigenen package.json zu erstellen. Nennen wir den Ordner beispielsweise Integrationstestskripte und legen dort drei Dateien ab: server.js, create-index.js, package.json. Die ersten beiden Dateien wurden oben beschrieben. Jetzt analysieren wir den Inhalt von 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"
 }
}


Die package.json enthält nur die Abhängigkeiten, die zum Ausführen von Integrationstests ( mit Unterstützung für Typoskript- und Screenshot-Tests) erforderlich sind, sowie Skripts zum Starten des Servers, zum Erstellen von index.html und des Startservers und -tests, die aus dem Kapitel zum Ausführen von Integrationstests in Angular Workspace bekannt sind ...



Starten



Wir verpacken die Ausführung von Integrationstests in eine neue Docker - Datei - Integration-Tests-ci.Docker-Datei :



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 []


Das Fazit ist einfach: Kopieren und erweitern Sie den Ordner "Integrationstests-Skripte" in das Stammverzeichnis der Anwendung und kopieren Sie alles, was zum Ausführen der Tests erforderlich ist (dieser Satz kann für Sie unterschiedlich sein). Die Hauptunterschiede zur vorherigen Datei bestehen darin, dass nicht die gesamte Anwendung in den Docker-Container kopiert wird, sondern nur eine minimale Optimierung der Testausführungszeit in CI.



Erstellen Sie eine Integrations- tests- ci.sh- Datei mit den folgenden Inhalten:



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


Wenn der Befehl mit Tests ausgeführt wird, wird package.json aus dem Ordner "Integrationstests-Skripte" zum Stammverzeichnis und der Befehl "main-app: integrations" wird darin gestartet. Da dieser Ordner zum Stammverzeichnis erweitert wird, sollten die Pfade zum Ordner mit der Statik Ihrer Anwendung mit der Idee angegeben werden, dass alles vom Stammverzeichnis und nicht vom Ordner "Integrationstests-Skripte" aus gestartet wird.



Ich möchte auch eine kleine Bemerkung machen: Das endgültige Bash-Skript zum Ausführen von Integrationstests, wie es sich entwickelt hat, habe ich anders genannt. Sie müssen dies nicht tun, es wurde nur zum bequemen Lesen dieses Artikels durchgeführt. Sie sollten immer noch eine Datei übrig haben, zum Beispiel integrations-tests.sh, die Sie bereits entwickeln. Wenn Sie mehrere Anwendungen im Repository haben und deren Vorbereitungsmethoden unterschiedlich sind, können Sie beide mit Variablen in bash auflösenoder verschiedene Dateien für jede Anwendung - je nach Ihren Anforderungen.



Zusammenfassung



Es gab viele Informationen - ich denke, jetzt lohnt es sich, auf der Grundlage dessen, was oben geschrieben wurde, zusammenzufassen.

Vorbereiten von Tools für lokales Schreiben und Ausführen von Tests mit einer Prise Screenshot-Tests:



  1. Abhängigkeit für Cypress hinzufügen.
  2. Bereiten Sie den Ordner mit Tests vor:

    1. Angular Single Application - Belassen Sie alles im Cypress-Ordner.
    2. Angular Workspace - Erstellen Sie neben der Anwendung, für die die Tests ausgeführt werden, eine Ordner-Anwendungsnamen-Integration, und verschieben Sie alles aus dem Cypress-Ordner in den Ordner.
    3. NX - Benennen Sie das Projekt von appname-e2e in appname-integrations um.


  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. In den Tests machen wir keine zufälligen Screenshots. Wenn dem Screenshot eine Animation vorangestellt ist, warten Sie darauf. Fügen Sie beispielsweise Cypress Assertion zum animierten Element hinzu.
    4. Das Datum wird entweder durch Cy.clock benetzt oder wir verwenden die Blackout-Option, wenn wir einen Screenshot machen.
    5. Wir erwarten alle geladenen Statiken zur Laufzeit über den benutzerdefinierten Befehl cy.waitForResource (Bilder, Schriftarten usw.).


  6. Packen Sie alles in Docker ein:

    1. Dockerfile vorbereiten.
    2. Wir erstellen eine Bash-Datei.




 Wir führen Tests über der zusammengebauten Anwendung durch:



  1. In CI lernen wir, Artefakte der zusammengestellten Anwendung zwischen Builds zu werfen (es liegt an Ihnen).
  2. Vorbereiten des Ordners für Integrationstestskripte:

    1. Ein Skript zum Hochfahren des Servers Ihrer Anwendung.
    2. Das Skript zum Ändern Ihrer index.html (wenn Sie mit der ursprünglichen index.html zufrieden sind, können Sie es überspringen).
    3. Fügen Sie dem Ordner package.json die erforderlichen Skripte und Abhängigkeiten hinzu.
    4. Vorbereiten einer neuen Docker-Datei.
    5. Wir erstellen eine Bash-Datei.


Nützliche Links



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

  2. Cypresstrade-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