Ideen zum Konfigurieren, Organisieren und Konsolidieren Ihrer Cypress-Tests #frontend@twiliosendgrid

Veröffentlicht: 2020-12-15

Wir haben hier bei Twilio SendGrid viele Cypress-Tests für verschiedene Webanwendungen und Frontend-Teams geschrieben. Als unsere Tests auf viele Funktionen und Seiten skaliert wurden, stießen wir auf einige nützliche Konfigurationsoptionen und entwickelten Wege, um unsere wachsende Anzahl von Dateien, Selektoren für die Elemente unserer Seite und Timeout-Werte besser zu verwalten.

Wir möchten Ihnen diese Tipps und Ideen zum Konfigurieren, Organisieren und Konsolidieren Ihrer eigenen Cypress-bezogenen Dinge zeigen, also fühlen Sie sich frei, mit ihnen das zu tun, was für Sie und Ihr Team am besten funktioniert.

Dieser Blog-Beitrag geht davon aus, dass Sie über praktische Kenntnisse zu Cypress-Tests verfügen und nach Ideen zur besseren Pflege und Verbesserung Ihrer Cypress-Tests suchen. Wenn Sie jedoch neugierig sind, mehr über allgemeine Funktionen, Assertionen und Muster zu erfahren, die Sie möglicherweise nützlich finden, um Cypress-Tests für separate Umgebungen zu schreiben, können Sie stattdessen diesen 1000-Fuß-Blogpost mit einer Übersicht lesen.

Lassen Sie uns andernfalls weitermachen und Sie zunächst durch einige Konfigurationstipps für Cypress führen.

Konfigurieren Sie Ihre cypress.json

In der Datei cypress.json können Sie die gesamte Konfiguration für Ihre Cypress-Tests festlegen, z. B. Basis-Timeouts für Ihre Cypress-Befehle, Umgebungsvariablen und andere Eigenschaften.

Hier sind einige Tipps, die Sie in Ihrer Konfiguration ausprobieren können:

  • Optimieren Sie die Timeouts Ihrer Basisbefehle, damit Sie nicht immer { timeout: timeoutInMs } in Ihren Cypress-Befehlen hinzufügen müssen. Experimentieren Sie mit den Zahlen, bis Sie die richtige Balance für Einstellungen wie „defaultCommandTimeout“, „requestTimeout“ und „responseTimeout“ gefunden haben.
  • Wenn Ihr Test iFrames beinhaltet, müssen Sie höchstwahrscheinlich „chromeWebSecurity“ auf „false“ setzen, damit Sie in Ihrer Anwendung auf Cross-Origin-iFrames zugreifen können.
  • Versuchen Sie, Marketing-, Analyse- und Protokollierungsskripts von Drittanbietern zu blockieren, wenn Sie Ihre Cypress-Tests ausführen, um die Geschwindigkeit zu erhöhen und keine unerwünschten Ereignisse auszulösen. Mit der Eigenschaft „blacklistHosts“ (oder „blockHosts“-Eigenschaft in Cypress 5.0.0) können Sie ganz einfach eine Deny-Liste einrichten, die ein Array von String-Globs enthält, die mit den Hostpfaden von Drittanbietern übereinstimmen.
  • Passen Sie die standardmäßigen Viewport-Abmessungen mit „viewportWidth“ und „viewportHeight“ an, wenn Sie die Cypress-GUI öffnen, um die Dinge besser sehen zu können.
  • Wenn es in Docker Speicherprobleme für wichtige Tests gibt oder Sie helfen möchten, die Dinge effizienter zu gestalten, können Sie versuchen, „numTestsKeptInMemory“ auf eine kleinere Zahl als den Standardwert zu ändern und „videoUploadOnPasses“ auf „false“ zu setzen, um sich auf das Hochladen von Videos für fehlgeschlagene Tests zu konzentrieren läuft nur.

Außerdem können Sie nach dem Optimieren Ihrer Cypress-Konfiguration auch TypeScript hinzufügen und Ihre Cypress-Tests besser eingeben, wie wir es in diesem Blogbeitrag getan haben. Dies ist besonders vorteilhaft für die automatische Vervollständigung, Typwarnungen oder Fehler beim Aufrufen und Verketten von Funktionen (wie cy.task('someTaskPluginFunction) , Cypress.env('someEnvVariable') oder cy.customCommand() ).

Sie können auch damit experimentieren, eine cypress.json -Basisdatei einzurichten und eine separate Cypress-Konfigurationsdatei für jede Testumgebung zu laden, z. B. eine staging.json , wenn Sie verschiedene Cypress-Skripts in Ihrer package.json . Die Cypress-Dokumentation führt Sie hervorragend durch diesen Ansatz.

Organisation Ihres Cypress-Ordners

Cypress richtet bereits einen cypress -Ordner der obersten Ebene mit einer geführten Struktur ein, wenn Sie Cypress zum ersten Mal in Ihrer Codebasis starten. Dazu gehören andere Ordner wie integration für Ihre Spezifikationsdateien, fixtures für JSON-Datendateien, plugins für Ihre cy.task(...) Funktionen und andere Konfigurationen sowie ein support -Ordner für Ihre benutzerdefinierten Befehle und Typen.

Eine gute Faustregel, die Sie beim Organisieren in Ihren Cypress-Ordnern, React-Komponenten oder jedem Code im Allgemeinen befolgen sollten, besteht darin, Dinge zusammenzufassen, die sich wahrscheinlich gemeinsam ändern. Da wir es in diesem Fall mit Webanwendungen im Browser zu tun haben, besteht ein Ansatz, der sich gut skalieren lässt, darin, Dinge in einem Ordner nach Seiten oder übergreifenden Funktionen zu gruppieren.

Wir haben einen separaten Seitenordner erstellt, der nach Funktionsnamen wie „ pages “ partitioniert ist, um alle Seitenobjekte unterhalb der /settings/sender_auth Route wie „DomainAuthentication“ (/settings/sender_auth/domains/**/*) und „LinkBranding“ zu speichern. (/settings/sender_auth/links/**/*). In unserem plugins -in-Ordner haben wir dasselbe getan, indem wir alle cy.task(...) -in-Dateien für eine bestimmte Funktion oder Seite im selben Ordner für die Absenderauthentifizierung organisiert haben, und wir haben den gleichen Ansatz für unseren integration für verfolgt die spec-Dateien. Wir haben unsere Tests auf Hunderte von Spezifikationsdateien, Seitenobjekten, Plugins und anderen Dateien in einer unserer Codebasen skaliert – und es ist einfach und bequem, durch sie zu navigieren.

Hier ist ein Überblick darüber, wie wir den cypress -Ordner organisiert haben.

Eine weitere Sache, die Sie beim Organisieren Ihres integration (in dem sich alle Ihre Spezifikationen befinden) berücksichtigen sollten, ist die potenzielle Aufteilung von Spezifikationsdateien basierend auf der Priorität des Tests. Beispielsweise können Sie alle Tests mit der höchsten Priorität und den höchsten Werten in einem Ordner „P1“ und die Tests mit der zweiten Priorität in einem Ordner „P2“ haben, sodass Sie alle „P1“-Tests einfach ausführen können, indem Sie die Option --spec wie --spec 'cypress/integration/P1/**/*' .

Erstellen Sie eine Spezifikationsordnerhierarchie, die für Sie funktioniert, damit Sie Spezifikationen nicht nur nach Seite oder Funktion wie --spec 'cypress/integration/SomePage/**/*' können, sondern auch nach einigen anderen Kriterien wie Priorität, Produkt , oder Umwelt.

Konsolidierung von Elementselektoren

Bei der Entwicklung der React-Komponenten unserer Seite fügen wir normalerweise einige Unit- und Integrationstests mit Jest und Enzyme hinzu. Gegen Ende der Funktionsentwicklung fügen wir eine weitere Ebene von Cypress E2E-Tests hinzu, um sicherzustellen, dass alles mit dem Backend funktioniert. Sowohl die Einheitentests für unsere React-Komponenten als auch die Seitenobjekte für unsere Cypress E2E-Tests erfordern Selektoren für die Komponenten/DOM-Elemente auf der Seite, mit der wir interagieren und auf der wir Assertion durchführen möchten.

Wenn wir diese Seiten und Komponenten aktualisieren, besteht die Möglichkeit, dass Abweichungen und Fehler auftreten, da mehrere Stellen von Einheitentest-Selektoren über das Cypress-Seitenobjekt mit dem eigentlichen Komponentencode selbst synchronisiert werden müssen. Wenn wir uns ausschließlich auf Klassennamen verlassen würden, die sich auf Stile beziehen, wäre es mühsam, daran zu denken, alle Stellen zu aktualisieren, die möglicherweise brechen. Stattdessen fügen wir „data-hook“, „data-testid“ oder jedes andere konsistent benannte „data-*“-Attribut zu bestimmten Komponenten und Elementen auf der Seite hinzu, die wir uns merken möchten, und schreiben einen Selektor dafür in unsere Testebenen.

Wir können „Datenhaken“-Attribute an viele unserer Elemente anhängen, aber wir brauchten noch eine Möglichkeit, sie alle an einem Ort zusammenzufassen, um sie zu aktualisieren und in anderen Dateien wiederzuverwenden. Wir haben einen typisierten Weg gefunden, um all diese „Data-Hook“-Selektoren zu verwalten, um sie auf unsere React-Komponenten zu verteilen und in unseren Komponententests und Seitenobjektselektoren für eine bessere Wiederverwendung und einfachere Wartung in exportierten Objekten zu verwenden.

Für den Ordner der obersten Ebene jeder Seite würden wir eine hooks.ts -Datei erstellen, die ein Objekt mit einem lesbaren Schlüsselnamen für das Element und dem eigentlichen String-CSS-Selektor für das Element als Wert verwaltet und exportiert. Wir nennen diese „Leseselektoren“, da wir das CSS-Selektorformular für ein Element in Aufrufen wie wrapper.find(“[data-hook='selector']”) von Enzyme in unseren Unit-Tests oder denen von Cypress lesen und verwenden müssen cy.get(“[data-hook='selector']”) in unseren Seitenobjekten. Diese Aufrufe würden dann sauberer aussehen wie wrapper.find(Selectors.someElement) oder cy.get(Selectors.someElement) .

Das folgende Code-Snippet behandelt mehr darüber, warum und wie wir diese Leseselektoren in der Praxis verwenden.

Auf ähnliche Weise exportieren wir auch Objekte mit einem lesbaren Schlüsselnamen für das Element und mit einem Objekt wie { “data-hook”: “selector” } als Wert. Wir werden diese „Write Selectors“ nennen, da wir diese Objekte als Requisiten auf eine React-Komponente schreiben oder verteilen müssen, um das „Data-Hook“-Attribut erfolgreich zu den zugrunde liegenden Elementen hinzuzufügen. Das Ziel wäre, so etwas zu tun und das eigentliche DOM-Element darunter – vorausgesetzt, Props werden korrekt an die JSX-Elemente weitergegeben – wird auch das Attribut „data-hook=“ gesetzt haben.

Das folgende Code-Snippet behandelt mehr darüber, warum und wie wir diese Schreibselektoren in der Praxis verwenden.

Wir können Objekte erstellen, um unsere Leseselektoren zu konsolidieren, und Schreibselektoren, um weniger Stellen zu aktualisieren, aber was wäre, wenn wir viele Selektoren für einige unserer komplexeren Seiten schreiben müssten? Dies kann fehleranfälliger sein, wenn Sie es selbst erstellen. Lassen Sie uns also Funktionen erstellen, um diese Leseselektoren und Schreibselektoren einfach zu generieren, um sie schließlich für eine bestimmte Seite zu exportieren.

Für die Leseselektor-Generatorfunktion durchlaufen wir die Eigenschaften eines Eingabeobjekts und bilden die CSS-Selektorzeichenfolge [data-hook=”selector”] für jeden Elementnamen. Wenn der entsprechende Wert eines Schlüssels null ist, gehen wir davon aus, dass der Elementname im Eingabeobjekt derselbe ist wie der „data-hook“-Wert, z. B. { someElement: null } => { someElement: '[data-hook=”someElement”] } . Andernfalls, wenn der entsprechende Wert eines Schlüssels nicht null ist, können wir diesen „Datenhaken“-Wert überschreiben, z. B. { someElement: “newSelector” } => { someElement: '[data-hook=”newSelector”]' } .

Für die Schreibselektor-Generatorfunktion durchlaufen wir die Eigenschaften eines Eingabeobjekts und bilden die Objekte { “data-hook”: “selector” } für jeden Elementnamen. Wenn der entsprechende Wert eines Schlüssels null ist, gehen wir davon aus, dass der Elementname im Eingabeobjekt derselbe ist wie der „data-hook“-Wert, z. B. { someElement: null } => { someElement: { “data-hook”: “someElement” } } . Andernfalls, wenn der entsprechende Wert eines Schlüssels nicht null ist, können wir diesen „Dateneinstieg“-Wert überschreiben, z. B. { someElement: “newSelector” } => { someElement: { “data-hook”: “newSelector” }' } .

Mit diesen beiden Generatorfunktionen erstellen wir unsere Leseselektor- und Schreibselektor-Objekte für eine Seite und exportieren sie zur Wiederverwendung in unseren Komponententests und Cypress-Seitenobjekten. Ein weiterer Bonus ist, dass diese Objekte so typisiert sind, dass wir Selectors.unknownElement oder WriteSelectors.unknownElement nicht versehentlich auch in unseren TypeScript-Dateien verwenden können. Vor dem Exportieren unserer Leseselektoren erlauben wir auch das Hinzufügen zusätzlicher Element- und CSS-Selektorzuordnungen für Komponenten von Drittanbietern, über die wir keine Kontrolle haben. In einigen Fällen können wir bestimmten Elementen kein „Data-Hook“-Attribut hinzufügen, daher müssen wir immer noch nach anderen Attributen, IDs und Klassen auswählen und dem Leseselektorobjekt weitere Eigenschaften hinzufügen, wie unten gezeigt.

Dieses Muster hilft uns, mit all unseren Selektoren für eine Seite organisiert zu bleiben und wann wir Dinge aktualisieren müssen. Wir empfehlen Ihnen, Möglichkeiten zu untersuchen, wie Sie all diese Selektoren in einer Art Objekt oder auf andere Weise verwalten können, um die Anzahl der Dateien zu minimieren, die Sie bei zukünftigen Änderungen berühren müssen.

Zeitüberschreitungen konsolidieren

Eine Sache, die uns nach dem Schreiben vieler Cypress-Tests aufgefallen ist, war, dass unsere Selektoren für bestimmte Elemente eine Zeitüberschreitung verursachten und länger dauerten als die in unserer cypress.json festgelegten Standard-Zeitüberschreitungswerte wie „defaultCommandTimeout“ und „responseTimeout“. Wir gingen zurück und passten bestimmte Seiten an, die längere Zeitüberschreitungen benötigten, aber im Laufe der Zeit wuchs die Anzahl willkürlicher Zeitüberschreitungswerte und die Wartung wurde bei großen Änderungen schwieriger.

Infolgedessen haben wir unsere Timeouts in einem Objekt konsolidiert, das oberhalb unseres „defaultCommandTimeout“ beginnt, das irgendwo im Bereich von fünf bis zehn Sekunden liegt, um die meisten allgemeinen Timeouts für unsere Selektoren wie cy.get(...) oder abzudecken cy.contains(...) . Über dieses Standard-Timeout hinaus würden wir innerhalb eines Timeout-Objekts, das wir überall in unsere Dateien importieren können, um es in unseren Cypress-Befehlen wie cy.get(“someElement”, { timeout: timeouts.short }) zu verwenden, auf „kurz“, „mittel“, „lang“, „xlong“ und „xxlong“ hochskalieren cy.get(“someElement”, { timeout: timeouts.short }) oder cy.task('pluginName', {}, { timeout: timeouts.xlong }) . Nachdem wir unsere Timeouts durch diese Werte aus unserem importierten Objekt ersetzt haben, haben wir einen Ort zum Aktualisieren, um die Zeit, die für bestimmte Timeouts benötigt wird, zu vergrößern oder zu verkleinern.

Ein Beispiel, wie Sie damit einfach anfangen können, sehen Sie unten.

Einpacken

Wenn Ihre Cypress-Testsuiten wachsen, können Sie auf einige der gleichen Probleme stoßen, die wir hatten, als wir herausfanden, wie Sie Ihre Cypress-Tests am besten skalieren und warten können. Sie können Ihre Dateien nach Seite, Funktion oder einer anderen Gruppierungskonvention organisieren, sodass Sie immer wissen, wo Sie nach vorhandenen Testdateien suchen und wo Sie neue hinzufügen müssen, wenn mehr Entwickler zu Ihrer Codebasis beitragen.

Wenn sich die Benutzeroberfläche ändert, können Sie eine Art typisiertes Objekt (z. B. die Lese- und Schreibselektoren) verwenden, um Ihre „Daten“-Attributselektoren für die Schlüsselelemente jeder Seite beizubehalten, auf denen Sie Ihre Einheitentests und Cypress bestätigen oder mit denen Sie interagieren möchten Prüfungen. Wenn Sie anfangen, zu viele willkürliche Werte für Dinge wie Timeout-Werte für Ihre Cypress-Befehle anzuwenden, ist es möglicherweise an der Zeit, ein Objekt einzurichten, das mit skalierten Werten gefüllt ist, damit Sie diese Werte an einer Stelle aktualisieren können.

Wenn sich die Dinge in Ihrer Frontend-Benutzeroberfläche, Backend-API und Cypress-Tests ändern, sollten Sie immer darüber nachdenken, wie Sie diese Tests einfacher verwalten können. Weniger Orte zum Aktualisieren und Fehler machen und weniger Unklarheit darüber, wo Dinge abgelegt werden sollen, machen einen großen Unterschied, da viele Entwickler später neue Seiten, Funktionen und (unvermeidlich) Cypress-Tests hinzufügen.

Interessiert an weiteren Beiträgen auf Cypress? Schauen Sie sich die folgenden Artikel an:

  • Was beim Schreiben von E2E-Tests zu beachten ist
  • 1.000-Fuß-Übersicht über das Schreiben von Cypress-Tests
  • Schreiben Sie alle Dinge in Ihre Cypress-Tests
  • Umgang mit E-Mail-Flows in Cypress-Tests
  • Integration von Cypress-Tests mit Docker, Buildkite und CICD