Frontend-Test

Eine effektive Strategie für automatisierte Codetests ist äußerst wichtig, um die schnelle und qualitativ hochwertige Arbeit von Teams von Programmierern zu gewährleisten, die an der Unterstützung und Entwicklung von Webprojekten beteiligt sind. Der Autor des Artikels sagt, dass in der Firma StackPath , in der er arbeitet, jetzt alles gut mit dem Testen funktioniert. Sie haben viele Tools zum Überprüfen von Code. Aus einer solchen Vielfalt muss man aber jeweils das auswählen, was am besten geeignet ist. Dies ist ein separates Problem. Und nachdem die erforderlichen Werkzeuge ausgewählt wurden, müssen Sie noch eine Entscheidung über die Reihenfolge ihrer Verwendung treffen.
Bild
Der Autor des Artikels gibt an, dass StackPath mit dem Vertrauen in die Qualität des Codes zufrieden ist, das dank des angewendeten Testsystems erreicht wurde. Hier möchte er eine Beschreibung der vom Unternehmen entwickelten Testprinzipien teilen und über die verwendeten Tools sprechen.

Testprinzipien


Bevor wir über bestimmte Tools sprechen, sollten wir über die Antwort auf die Frage nachdenken, was gute Tests sind. Bevor wir mit der Arbeit an unserem Portal für Kunden beginnen, haben wir die Grundsätze formuliert und niedergeschrieben, die wir bei der Erstellung von Tests befolgen möchten. Was wir in erster Linie getan haben, hat uns bei der Auswahl der Werkzeuge geholfen.

Hier sind die vier Prinzipien in Frage.

▍ Prinzip Nummer 1. Tests sind als Optimierungsaufgaben zu verstehen


Eine effektive Teststrategie besteht darin, das Problem der Maximierung eines bestimmten Werts (in diesem Fall des Vertrauens, dass die Anwendung ordnungsgemäß funktioniert) und der Minimierung bestimmter Kosten zu lösen (hier werden die "Kosten" durch die für die Unterstützung und Ausführung der Tests erforderliche Zeit dargestellt). Beim Schreiben von Tests stellen wir häufig die folgenden Fragen in Bezug auf das oben beschriebene Prinzip:

  • Wie hoch ist die Wahrscheinlichkeit, dass bei diesem Test ein Fehler auftritt?
  • Verbessert dieser Test unser Testsystem und sind die Kosten für die zum Schreiben erforderlichen Ressourcen den daraus resultierenden Nutzen wert?
  • Ist es möglich, dasselbe Maß an Vertrauen in die zu testende Entität zu erlangen, das dieser Test bietet, indem ein weiterer Test erstellt wird, der einfacher zu schreiben, zu warten und auszuführen ist?

▍ Grundsatz Nr. 2. Ein übermäßiger Gebrauch von Mox sollte vermieden werden.


Eine meiner Lieblingserklärungen zum Konzept von „mok“ fand sich in dieser Präsentation auf der Konferenz von Assert.js 2018. Der Redner hat die Frage ausführlicher erörtert, als ich sie hier eröffnen werde. In der Rede wird die Entstehung von Mokas mit dem „Schlagen von Löchern in die Realität“ verglichen. Und ich denke, dass dies eine sehr visuelle Art ist, Moks wahrzunehmen. Obwohl es in unseren Tests Mokas gibt, vergleichen wir die Verringerung der „Kosten“ von Tests, die Mokas aufgrund der Vereinfachung des Prozesses des Schreibens und Ausführens von Tests bereitstellt, mit der Verringerung des Werts der Tests, die dazu führen, dass in der Realität ein weiteres Loch gemacht wird.

Zuvor verließen sich unsere Programmierer stark auf Komponententests, die so geschrieben wurden, dass alle untergeordneten Abhängigkeiten durch Mokas ersetzt wurden, die die API für die Wiedergabe flacher Enzyme verwendeten . Auf diese Weise gerenderte Entitäten wurden dann mit Jest- Snapshots überprüft. Alle diese Tests wurden nach einem ähnlichen Muster geschrieben:

it('renders ', () => {   const wrapper = shallow();   //  ,              expect(wrapper).toMatchSnapshot(); }); 

Diese Tests sind an vielen Stellen mit der Realität gefüllt. Dieser Ansatz macht es sehr einfach, mit Tests eine 100% ige Codeabdeckung zu erzielen. Wenn Sie solche Tests schreiben, müssen Sie sehr wenig darüber nachdenken, aber wenn Sie nicht alle zahlreichen Integrationspunkte überprüfen, sind solche Tests nicht besonders wertvoll. Alle Tests können erfolgreich abgeschlossen werden, aber dies gibt nicht viel Vertrauen in die Funktionsfähigkeit der Anwendung. Und noch schlimmer, alle Mokas haben einen versteckten "Preis", den Sie bezahlen müssen, nachdem die Tests geschrieben wurden.

▍ Grundsatz Nr. 3. Tests sollten das Refactoring von Code erleichtern, nicht erschweren.


Tests wie der oben gezeigte erschweren das Refactoring. Wenn sich an vielen Stellen im Projekt doppelter Code befindet und ich diesen Code nach einer Weile als separate Komponente formatiere, schlagen alle Tests für die Komponenten, in denen ich diese neue Komponente verwenden werde, fehl. Komponenten, die mithilfe der Flachrendertechnik abgeleitet wurden, sind bereits etwas anderes. Wo ich früher Markups wiederholt habe, gibt es jetzt eine neue Komponente.

Komplexeres Refactoring, bei dem einige Komponenten zu einem Projekt hinzugefügt und andere entfernt werden, führt zu noch größerer Verwirrung. Tatsache ist, dass Sie dem System neue Tests hinzufügen und unnötige Tests daraus entfernen müssen. Die Snapshot-Regeneration ist eine einfache Aufgabe, aber welchen Wert haben solche Tests? Selbst wenn sie in der Lage sind, einen Fehler zu finden, ist es besser, wenn sie ihn in einer Reihe von Schnappschussänderungen verpassen und einfach neue Schnappschüsse überprüfen, ohne zu viel Zeit darauf zu verwenden.

Infolgedessen helfen solche Tests nicht besonders beim Refactoring. Im Idealfall sollte kein Test fehlschlagen, wenn ich ein Refactoring durchführe. Danach hat sich nichts geändert, was der Benutzer sieht und mit dem er interagiert. Und umgekehrt - wenn ich ändere, was der Benutzer kontaktiert, sollte mindestens ein Test fehlschlagen. Wenn Tests diesen beiden Regeln folgen, sind sie ein hervorragendes Werkzeug, um sicherzustellen, dass sich etwas, auf das Benutzer stoßen, während des Refactorings nicht ändert.

▍ Grundsatz Nr. 4. Tests sollten nachvollziehen, wie echte Benutzer mit der Anwendung arbeiten


Ich möchte, dass die Tests nur dann fehlschlagen, wenn sich etwas geändert hat, mit dem der Benutzer interagiert. Das bedeutet, dass Tests mit der Anwendung genauso funktionieren sollten, wie Benutzer damit arbeiten. Beispielsweise muss ein Test wirklich mit Formularelementen interagieren und wie ein Benutzer Text in Texteingabefelder eingeben. Tests sollten nicht auf Komponenten zugreifen und Methoden ihres Lebenszyklus unabhängig aufrufen, sollten nichts in den Status von Komponenten schreiben oder etwas tun, das von den Feinheiten der Komponentenimplementierung abhängt. Da ich letztendlich den Teil des Systems überprüfen möchte, der mit dem Benutzer in Kontakt steht, ist es logisch, sicherzustellen, dass die Tests bei der Interaktion mit dem System die Aktionen der tatsächlichen Benutzer so genau wie möglich reproduzieren.

Werkzeuge testen


Nachdem wir die Ziele definiert haben, die wir erreichen möchten, wollen wir uns darüber unterhalten, welche Tools wir dafür ausgewählt haben.

▍TypeScript


Unsere Codebasis verwendet TypeScript. Unsere Backend-Services sind in Go geschrieben und interagieren mithilfe von gRPC miteinander. Dies ermöglicht es uns, typisierte gRPC-Clients für die Verwendung auf einem GraphQL-Server zu generieren. GraphQL-Server-Resolver werden mit Typen getippt, die mit dem graphql-Code-Generator generiert wurden. Und schließlich sind unsere Abfragen, Mutationen sowie Abonnementkomponenten und Hooks vollständig typisiert. Die vollständige Abdeckung unserer Codebasis mit Typen beseitigt eine ganze Klasse von Fehlern, die dadurch verursacht werden, dass das Datenformular nicht den Erwartungen des Programmierers entspricht. Die Generierung von Typen aus den Schema- und Protobuf-Dateien stellt sicher, dass unser gesamtes System in allen Teilen des verwendeten Technologiestapels homogen bleibt.

▍Jest (Unit-Test)


Als Framework zum Testen von Code verwenden wir Jest und @ testing-library / react . In Tests, die mit diesen Tools erstellt wurden, testen wir Funktionen oder Komponenten isoliert vom Rest des Systems. Normalerweise testen wir Funktionen und Komponenten, die am häufigsten in einer Anwendung verwendet werden, oder solche, die viele Möglichkeiten haben, Code auszuführen. Solche Pfade sind während der Integration oder der End-to-End-Tests (E2E) nur schwer zu überprüfen.

Unit Tests sind für uns ein Mittel, um Kleinteile zu testen. Integrations- und End-to-End-Tests ermöglichen eine hervorragende Überprüfung des Systems in größerem Maßstab, sodass Sie den Gesamtzustand der Anwendung überprüfen können. Aber manchmal müssen Sie sicherstellen, dass die kleinen Details funktionieren, und das Schreiben von Integrationstests für alle möglichen Verwendungen des Codes ist zu teuer.

Beispielsweise müssen wir überprüfen, ob die Tastaturnavigation in der Komponente funktioniert, die für die Arbeit mit der Dropdown-Liste verantwortlich ist. Gleichzeitig möchten wir aber nicht alle möglichen Varianten eines solchen Verhaltens beim Testen der gesamten Anwendung überprüfen. Aus diesem Grund testen wir die Navigation einzeln gründlich und achten beim Testen von Seiten mit der entsprechenden Komponente nur auf die Überprüfung von Interaktionen auf höherer Ebene.

Werkzeuge testen


▍Cypress (Integrationstests)


Mit Cypress erstellte Integrationstests bilden den Kern unseres Testsystems. Als wir mit der Erstellung des StackPath-Portals begannen, waren dies die ersten Tests, die wir geschrieben haben, da sie mit sehr geringem Aufwand für ihre Erstellung äußerst wertvoll sind. Cypress zeigt unsere gesamte Anwendung in einem Browser an und führt Testskripte aus. Unser gesamtes Frontend funktioniert genauso, wie Benutzer damit arbeiten. Richtig, die Netzwerkschicht des Systems wird durch Mokami ersetzt. Jede Netzwerkabfrage, die normalerweise an den GraphQL-Server gesendet wird, gibt bedingte Daten an die Anwendung zurück.

Die Verwendung von Mocks zur Simulation der Netzwerkschicht einer Anwendung hat viele Stärken:

  • Tests sind schneller. Selbst wenn das Projekt-Backend extrem schnell ist, kann die Zeit, die erforderlich ist, um Antworten auf Anforderungen, die während der gesamten Testsuite gestellt wurden, zurückzugeben, beträchtlich sein. Und wenn Moki für die Rückgabe von Antworten verantwortlich ist, werden die Antworten sofort zurückgegeben.
  • Tests werden zuverlässiger. Eine der Schwierigkeiten beim Durchführen eines vollständigen End-to-End-Tests eines Projekts besteht darin, dass der variable Status der Netzwerk- und Serverdaten berücksichtigt werden muss, der sich ändern kann. Wenn mit Moxas ein echter Netzwerkzugriff simuliert wird, verschwindet diese Variabilität.
  • Es ist einfach, Situationen zu reproduzieren, die die exakte Wiederholung bestimmter Bedingungen erfordern. In einem realen System ist es beispielsweise schwierig, bestimmte Anforderungen zum Scheitern zu bringen. Wenn Sie die korrekte Reaktion der Anwendung auf erfolglose Anfragen überprüfen müssen, können Sie mit moki problemlos Notfallsituationen reproduzieren.

Obwohl das Ersetzen des gesamten Backends durch mok eine entmutigende Aufgabe ist, werden alle bedingten Daten mit denselben generierten TypeScript-Typen eingegeben, die in der Anwendung verwendet werden. Das heißt - zumindest diese Daten - entsprechen strukturell garantiert dem, was ein normales Backend zurückgeben würde. Während der meisten Tests haben wir uns ziemlich friedlich damit abgefunden, Mooks anstelle von echten Serveraufrufen zu verwenden.

Darüber hinaus sind Programmierer sehr erfreut, mit Cypress zu arbeiten. Tests werden im Cypress Test Runner ausgeführt. Testbeschreibungen werden links angezeigt, und die Testanwendung wird im iframe Hauptelement ausgeführt. Nachdem Sie den Test gestartet haben, können Sie seine einzelnen Phasen studieren und herausfinden, wie sich die Anwendung zu dem einen oder anderen Zeitpunkt verhalten hat. Da das Tool zum Ausführen von Tests im Browser selbst ausgeführt wird, können Sie die Tests mit den Browser-Tools des Entwicklers debuggen.

Wenn Sie Front-End-Tests schreiben, dauert es oft sehr lange, bis Sie die Ergebnisse des Tests mit dem Status des DOM zu einem bestimmten Zeitpunkt des Tests verglichen haben. Cypress vereinfacht diese Aufgabe erheblich, da der Entwickler alles sehen kann, was mit der getesteten Anwendung geschieht. Hier ist ein Videoclip, der dies demonstriert.

Diese Tests veranschaulichen unsere Testprinzipien perfekt. Das Verhältnis ihres Wertes zu ihrem "Preis" passt zu uns. Tests reproduzieren sehr ähnlich die Aktionen des realen Benutzers, der mit der Anwendung interagiert. Und nur die Netzwerkschicht des Projekts wurde durch Mokami ersetzt.

▍Cypress (End-to-End-Test)


Unsere E2E-Tests werden ebenfalls mit Cypress geschrieben. In diesen Tests wird Moki jedoch weder zur Simulation der Netzwerkebene eines Projekts noch zur Simulation anderer Funktionen verwendet. Bei der Durchführung von Tests greift die Anwendung auf den realen GraphQL-Server zu, der mit realen Instanzen von Backend-Diensten arbeitet.

End-to-End-Tests sind für uns äußerst wertvoll. Tatsache ist, dass es die Ergebnisse solcher Tests sind, die uns wissen lassen, ob etwas wie erwartet funktioniert oder nicht. Bei solchen Tests werden keine Mocks verwendet. Daher funktioniert die Anwendung genau so, wie sie von echten Clients verwendet wird. Es ist jedoch zu beachten, dass End-to-End-Tests „teurer“ sind als andere. Sie sind langsamer und schwieriger zu schreiben, da bei ihrer Implementierung kurzfristige Fehler auftreten können. Es sind weitere Arbeiten erforderlich, um sicherzustellen, dass das System in einem bekannten Zustand bleibt, bevor die Tests ausgeführt werden.

Tests müssen normalerweise zu einem Zeitpunkt ausgeführt werden, an dem sich das System in einem bekannten Zustand befindet. Nach Abschluss des Tests wechselt das System in einen anderen bekannten Zustand. Im Fall von Integrationstests ist es nicht schwierig, dieses Verhalten des Systems zu erreichen, da Aufrufe der API durch Mokas ersetzt werden und folglich jeder Test unter vorbestimmten Bedingungen ausgeführt wird, die vom Programmierer gesteuert werden. Bei E2E-Tests ist dies jedoch bereits schwieriger, da das Server Data Warehouse Informationen enthält, die sich während des Tests ändern können. Infolgedessen muss der Entwickler einen Weg finden, um sicherzustellen, dass sich das System zu Beginn des Tests in einem zuvor bekannten Zustand befindet.

Zu Beginn des End-to-End-Testlaufs führen wir ein Skript aus, das durch direkte Aufrufe der API ein neues Konto mit Stacks, Sites, Workloads, Monitoren und dergleichen erstellt. Jede Testsitzung impliziert die Verwendung einer neuen Instanz eines solchen Kontos, aber alles andere bleibt von Zeit zu Zeit unverändert. Nachdem das Skript alle erforderlichen Schritte ausgeführt hat, erstellt es eine Datei mit den Daten, die zum Ausführen der Tests verwendet werden (normalerweise enthält es Informationen zu Instanz-IDs und Domänen). Infolgedessen stellt sich heraus, dass Sie mit dem Skript das System in einen zuvor bekannten Zustand versetzen können, bevor Sie die Tests ausführen.

Da End-to-End-Tests „teurer“ sind als andere Testarten, schreiben wir im Vergleich zu Integrationstests weniger End-to-End-Tests. Wir bemühen uns sicherzustellen, dass die Tests kritische Anwendungsmerkmale abdecken. Dies ist beispielsweise das Registrieren von Benutzern und ihrer Anmeldung, das Erstellen und Einrichten einer Site / eines Workloads usw. Dank umfangreicher Integrationstests wissen wir, dass unser Frontend im Allgemeinen funktionsfähig ist. End-to-End-Tests sind jedoch nur erforderlich, um sicherzustellen, dass beim Verbinden des Frontends mit dem Backend nichts passiert, was andere Tests nicht erkennen können.

Nachteile unserer umfassenden Teststrategie


Obwohl wir mit den Tests und der Stabilität der Anwendung sehr zufrieden sind, hat die Verwendung einer umfassenden Teststrategie wie unserer auch Nachteile.

Zunächst einmal bedeutet die Anwendung einer solchen Teststrategie, dass alle Teammitglieder mit vielen Testwerkzeugen vertraut sein sollten und nicht nur mit einem. Jeder muss Jest, @ testing-library / react und Cypress kennen. Gleichzeitig müssen Entwickler diese Tools aber nicht nur kennen. Sie müssen auch in der Lage sein, Entscheidungen darüber zu treffen, in welcher Situation welche eingesetzt werden soll. Lohnt es sich, eine neue Möglichkeit zum Schreiben eines End-to-End-Tests zu testen, oder reicht der Integrationstest aus? Müssen Sie zusätzlich zum End-to-End- oder Integrationstest einen Komponententest schreiben, um die kleinen Details der Implementierung dieser neuen Funktion zu überprüfen?

Zweifellos „belastet“ dies sozusagen den Kopf unserer Programmierer, während sie das einzige Werkzeug verwenden, das sie nicht so stark belasten würden. Normalerweise beginnen wir mit Integrationstests. Wenn wir dann feststellen, dass das untersuchte Feature von besonderer Bedeutung ist und stark vom Serverteil des Projekts abhängt, fügen wir den entsprechenden End-to-End-Test hinzu. Oder wir beginnen mit Unit-Tests, wenn wir glauben, dass ein Unit-Test nicht alle Feinheiten der Implementierung eines bestimmten Mechanismus verifizieren kann.

Natürlich sind wir immer noch mit Situationen konfrontiert, in denen nicht klar ist, wo wir anfangen sollen. Da wir jedoch ständig Entscheidungen in Bezug auf Tests treffen müssen, tauchen bestimmte Muster gemeinsamer Situationen auf. Beispielsweise testen wir normalerweise Formularvalidierungssysteme mithilfe von Komponententests. Dies geschieht aufgrund der Tatsache, dass Sie während des Tests viele verschiedene Szenarien überprüfen müssen. Gleichzeitig weiß jeder im Team davon und verschwendet keine Zeit damit, eine Teststrategie zu planen, wenn einer von ihnen das Formularvalidierungssystem testen muss.

Ein weiterer Nachteil des von uns verwendeten Ansatzes ist die Schwierigkeit, Daten zur Codeabdeckung durch Tests zu sammeln. Obwohl dies möglich ist, ist es viel komplizierter als in einer Situation, in der ein Projekt getestet wird. Obwohl das Streben nach einer schönen Anzahl von Code-Coverages durch Tests zu einer Verschlechterung der Testqualität führen kann, sind solche Informationen für das Auffinden von „Lücken“ in der verwendeten Testsuite von Nutzen. Das Problem bei der Verwendung mehrerer Testtools besteht darin, dass Sie Berichte zur Codeabdeckung mit Tests kombinieren müssen, die von verschiedenen Systemen stammen, um zu verstehen, welcher Teil des Codes nicht getestet wurde. Es ist möglich, aber es ist definitiv viel schwieriger, als einen Bericht zu lesen, der mit einem Testverfahren erstellt wurde.

Zusammenfassung


Beim Einsatz vieler Testwerkzeuge standen wir vor schwierigen Aufgaben. Aber jedes dieser Werkzeuge erfüllt seinen eigenen Zweck. Letztendlich glauben wir, dass wir das Richtige getan haben, indem wir sie in unser Code-Testsystem aufgenommen haben. Integrationstests - Hier sollten Sie am besten zu Beginn der Arbeit an einer neuen Anwendung oder beim Ausrüsten von Tests eines vorhandenen Projekts mit dem Erstellen eines Testsystems beginnen. Es wird nützlich sein, zu versuchen, dem Projekt so früh wie möglich End-to-End-Tests hinzuzufügen, um die wichtigsten Funktionen des Projekts zu überprüfen.

Wenn die Testsuite End-to-End- und Integrationstests enthält, sollte dies dazu führen, dass der Entwickler ein gewisses Maß an Vertrauen in den Zustand der Anwendung erhält, wenn Änderungen daran vorgenommen werden. Wenn im Verlauf der Projektarbeiten Fehler auftauchen, die von den Tests nicht erkannt werden, ist zu überlegen, welche Tests diese Fehler auffangen könnten und ob das Auftreten von Fehlern auf die Mängel des gesamten im Projekt verwendeten Testsystems hindeutet.

Natürlich sind wir nicht sofort zu unserem aktuellen Testsystem gekommen. Darüber hinaus gehen wir davon aus, dass sich dieses System mit dem Wachstum unseres Projekts weiterentwickeln wird. Aber jetzt mögen wir unseren Ansatz zum Testen wirklich.

Sehr geehrte Leser! Welche Strategien verfolgen Sie beim Frontend-Testen? Welche Frontend-Testtools verwenden Sie?


Source: https://habr.com/ru/post/de477278/


All Articles