So schreiben Sie zuverlässige Browsertests mit Selenium und Node.js

Es gibt viele gute Artikel über den Einstieg in automatisierte Browsertests mit der NodeJS-Version von Selenium.

Einige wickeln die Tests in Mokka oder Jasmin ein, andere automatisieren alles mit npm oder Grunt oder Gulp. Sie alle beschreiben, wie Sie installieren, was Sie benötigen, und geben ein grundlegendes Beispiel für den Arbeitscode. Dies ist sehr hilfreich, da es eine Herausforderung sein kann, die verschiedenen Teile zum ersten Mal zum Laufen zu bringen.

Sie kennen jedoch nicht die Details der vielen Fallstricke und bewährten Methoden zur Automatisierung der Browsertests bei Verwendung von Selen.

Dieser Artikel wird fortgesetzt, wo diese anderen Artikel aufhören, und hilft Ihnen, automatisierte Browsertests zu schreiben, die mit der NodeJS-Selenium-API wesentlich zuverlässiger und wartbarer sind.

Vermeiden Sie zu schlafen

Die Selenium-Methode driver.sleep ist Ihr schlimmster Feind. Und jeder nutzt es. Dies kann daran liegen, dass die Dokumentation für die Node.js-Version von Selenium knapp ist und nur die API-Syntax abdeckt. Es fehlen reale Beispiele.

Oder weil viele Beispielcodes in Blogartikeln und auf Q & A-Sites wie StackOverflow davon Gebrauch machen.

Nehmen wir an, ein Bedienfeld wird von einer Größe von Null bis zur vollen Größe animiert. Lass uns nachsehen.

Es passiert so schnell, dass Sie möglicherweise nicht bemerken, dass sich die Größe und Position der Schaltflächen und Steuerelemente im Bedienfeld ständig ändern.

Hier ist eine verlangsamte Version. Achten Sie auf die grüne Schaltfläche zum Schließen und Sie können die sich ändernde Größe und Position des Bedienfelds sehen.

Dies ist für echte Benutzer kaum ein Problem, da die Animation so schnell abläuft. Wenn es langsam genug ist, wie im zweiten Video, und Sie versucht haben, manuell auf die Schaltfläche zum Schließen zu klicken, klicken Sie möglicherweise auf die falsche Schaltfläche oder verpassen die Schaltfläche ganz.

Aber diese Animationen geschehen normalerweise so schnell, dass Sie dazu keine Chance haben. Die Menschen warten nur, bis die Animation abgeschlossen ist. Nicht wahr mit Selen. Es ist so schnell, dass versucht werden kann, auf Elemente zu klicken, die noch animiert werden. Möglicherweise wird eine Fehlermeldung wie die folgende angezeigt:

System.InvalidOperationException: Element kann am Punkt (326, 792.5) nicht angeklickt werden

Dies ist der Zeitpunkt, an dem viele Programmierer sagen: „Aha! Ich muss warten, bis die Animation beendet ist, daher verwende ich nur driver.sleep (1000), um zu warten, bis das Bedienfeld verwendet werden kann. "

Also, was ist das Problem?

Die Anweisung driver.sleep (1000) macht, wie es aussieht. Die Ausführung Ihres Programms wird für 1000 Millisekunden angehalten, und der Browser kann weiterarbeiten. Layout, Einblenden oder Animieren von Elementen, Laden der Seite oder was auch immer.

Wenn das Panel im obigen Beispiel über einen Zeitraum von 800 Millisekunden eingeblendet wird, erreicht driver.sleep (1000) normalerweise das, was Sie möchten. Warum also nicht benutzen?

Der wichtigste Grund ist, dass es nicht deterministisch ist. Das heißt, es wird nur manchmal funktionieren. Da dies nur teilweise funktioniert, entstehen fragile Tests, die unter bestimmten Bedingungen abbrechen. Dies gibt automatisierten Browsertests einen schlechten Ruf.

Warum funktioniert es nur manchmal? Mit anderen Worten, warum ist es nicht deterministisch?

Was Sie mit Ihren Augen bemerken, ist nicht oft das einzige, was auf einer Website passiert. Ein Einblend- oder Animationselement ist ein perfektes Beispiel. Wir sollten diese Dinge nicht bemerken, wenn sie gut gemacht werden.

Wenn Sie Selenium anweisen, zuerst ein Element zu suchen und dann darauf zu klicken, kann es sein, dass zwischen diesen beiden Vorgängen nur einige Millisekunden liegen. Selen kann viel schneller sein als ein Mensch.

Wenn ein Mensch die Website nutzt, warten wir, bis das Element eingeblendet ist, bevor wir darauf klicken. Und wenn dieses Einblenden weniger als eine Sekunde dauert, bemerken wir wahrscheinlich nicht einmal, dass wir dieses „Warten“ machen. Selen ist nicht nur schneller und weniger fehlerverzeihend, Ihre automatisierten Tests müssen mit allen möglichen anderen unvorhersehbaren Faktoren fertig werden:

  1. Der Designer Ihrer Webseite ändert möglicherweise die Animationszeit von 800 Millisekunden auf 1200 Millisekunden. Ihr Test ist gerade gebrochen.
  2. Browser tun nicht immer genau das, wonach Sie fragen. Aufgrund der Systemlast wird die Animation möglicherweise angehalten und dauert länger als 800 Millisekunden, möglicherweise sogar länger als 1000 Millisekunden. Ihr Test ist gerade gebrochen.
  3. Unterschiedliche Browser haben unterschiedliche Layout-Engines und priorisieren die Layout-Vorgänge unterschiedlich. Fügen Sie Ihrer Testsuite einen neuen Browser hinzu, und Ihre Tests sind gerade gescheitert.
  4. Browser und das JavaScript, das eine Seite steuert, sind von Natur aus asynchron. Wenn die Animation in unserem Beispiel die Funktionalität ändert, für die Informationen vom Back-End benötigt werden, fügt der Programmierer möglicherweise einen AJAX-Aufruf hinzu und wartet auf das Ergebnis, bevor die Animation ausgelöst wird.
    Wir beschäftigen uns jetzt mit Netzwerklatenz und der Null-Garantie, wie lange es dauern wird, bis das Panel angezeigt wird. Ihr Test ist gerade gebrochen.
  5. Es gibt sicherlich noch andere Gründe, die ich nicht kenne.
    Sogar ein Browser für sich ist ein komplexes Tier, und alle haben Fehler. Wir wollen also versuchen, dasselbe für verschiedene Browser, verschiedene Browserversionen, verschiedene Betriebssysteme und verschiedene Betriebssystemversionen zum Laufen zu bringen.
    Irgendwann brechen Ihre Tests einfach ab, wenn sie nicht deterministisch sind. Kein Wunder, dass Programmierer auf automatisierte Browsertests verzichten und sich darüber beschweren, wie fragil die Tests sind.

Was tun Programmierer normalerweise, um Probleme zu beheben, wenn eines der oben genannten Probleme auftritt? Sie führen die Dinge auf Zeitprobleme zurück, daher ist die offensichtliche Antwort, die Zeit in der Anweisung driver.sleep zu verlängern. Drücken Sie dann die Daumen, um alle möglichen zukünftigen Szenarien der Systemauslastung, der Layout-Engine-Unterschiede usw. abzudecken. Es ist nicht deterministisch und bricht, also tu das nicht!

Wenn Sie noch nicht überzeugt sind, gibt es noch einen Grund: Ihre Tests werden viel schneller ausgeführt. Die Animation aus unserem Beispiel dauert hoffentlich nur 800 Millisekunden. Wenn Sie mit dem "Wir hoffen" fertig werden und die Tests unter allen Bedingungen funktionieren, werden Sie wahrscheinlich in der realen Welt so etwas wie driver.sleep (2000) sehen.

Das ist mehr als eine volle Sekunde Zeitverlust für nur einen Schritt Ihrer automatisierten Tests. Über viele Schritte summiert es sich schnell. Ein kürzlich überarbeiteter Test für eine unserer Webseiten, der wegen übermäßiger Nutzung von driver.sleep mehrere Minuten in Anspruch nahm, dauert jetzt weniger als fünfzehn Sekunden.

In den meisten weiteren Abschnitten dieses Artikels finden Sie konkrete Beispiele dafür, wie Sie Ihre Tests vollständig deterministisch gestalten und die Verwendung von driver.sleep vermeiden können.

Eine Notiz über Versprechen

Die JavaScript-API für Selen verwendet viel Versprechungen, und sie kann dies auch gut verbergen, indem sie einen integrierten Versprechungsmanager verwendet. Dies ändert sich und wird veraltet sein.

In Zukunft müssen Sie entweder lernen, wie Sie Versprechen verketten, oder Sie müssen die neuen asynchronen JavaScript-Funktionen wie wait verwenden.

In diesem Artikel wird in den Beispielen immer noch der traditionelle integrierte Selenium-Versprechen-Manager verwendet, und es wird die Verkettung von Versprechen genutzt. Die Codebeispiele hier sind sinnvoller, wenn Sie verstehen, wie Versprechen funktionieren. Aber Sie können noch viel aus diesem Artikel herausholen, wenn Sie Lernversprechen für den Moment auslassen möchten.

Lass uns anfangen

Wenn Sie mit unserem Beispiel einer Schaltfläche fortfahren, auf die Sie in einem animierten Bereich klicken möchten, schauen wir uns einige spezifische Fallstricke an, die unsere Tests unterbrechen könnten.

Wie wäre es mit einem Element, das dynamisch zur Seite hinzugefügt wird und noch nicht vorhanden ist, nachdem die Seite vollständig geladen wurde?

Warten auf das Vorhandensein eines Elements im DOM

Der folgende Code funktioniert nicht, wenn dem DOM nach dem Laden der Seite ein Element mit der CSS-ID "my-button" hinzugefügt wird:

// Selenium-Initialisierungscode aus Gründen der Übersichtlichkeit weggelassen
// Lade die Seite.
driver.get ('https: /foobar.baz');
// Finde das Element.
const button = driver.findElement (By.id ('my-button'));
button.click ();

Die driver.findElement-Methode erwartet, dass das Element bereits im DOM vorhanden ist. Es tritt ein Fehler auf, wenn das Element nicht sofort gefunden werden kann. In diesem Fall bedeutet sofort "Nach dem Laden der Seite" aufgrund der vorherigen driver.get-Anweisung.

Denken Sie daran, dass die aktuelle Version von JavaScript Selenium die Versprechen für Sie verwaltet. Daher wird jede Anweisung vollständig ausgeführt, bevor mit der nächsten Anweisung fortgefahren wird.

Hinweis: Das oben beschriebene Verhalten ist nicht immer unerwünscht. driver.findElement allein kann nützlich sein, wenn Sie sicher sind, dass das Element bereits vorhanden ist.

Schauen wir uns zunächst die falsche Art an, dies zu beheben. Nachdem wir erfahren haben, kann es einige Sekunden dauern, bis das Element zum DOM hinzugefügt wird:

driver.get ('https: /foobar.baz');
// Seite wurde geladen, jetzt für ein paar Sekunden schlafen gehen.
Fahrer.Schlaf (3000);
// Bete, dass drei Sekunden ausreichen und finde das Element.
const button = driver.findElement (By.id ('my-button'));
button.click ();

Aus allen zuvor genannten Gründen kann dies brechen und wird es wahrscheinlich auch tun. Wir müssen lernen, auf das Auffinden eines Elements zu warten. Dies ist recht einfach und wird häufig in Beispielen aus dem Internet gezeigt. Im folgenden Beispiel verwenden wir die gut dokumentierte driver.wait-Methode, um bis zu 20 Sekunden auf das Element im DOM zu warten:

const button = driver.wait (
  until.elementLocated (By.id ('my-button')),
  20000
);
button.click ();

Dies hat unmittelbare Vorteile. Wenn das Element beispielsweise in einer Sekunde zum DOM hinzugefügt wird, wird die driver.wait-Methode in einer Sekunde abgeschlossen. Es wird nicht die vollen 20 Sekunden warten, die angegeben werden.

Aufgrund dieses Verhaltens können wir eine Menge Padding in unser Timeout einfügen, ohne uns Gedanken darüber zu machen, dass das Timeout unsere Tests verlangsamt. Im Gegensatz zum driver.sleep, der immer die gesamte angegebene Zeit wartet.

Dies funktioniert in vielen Fällen. Ein Fall, in dem dies nicht funktioniert, ist der Versuch, auf ein Element zu klicken, das im DOM vorhanden, aber noch nicht sichtbar ist.

Selen ist intelligent genug, um kein Element anzuklicken, das nicht sichtbar ist. Dies ist gut, da Benutzer nicht auf unsichtbare Elemente klicken können, aber wir arbeiten härter daran, zuverlässige automatisierte Tests zu erstellen.

Warten bis ein Element sichtbar ist

Wir werden auf dem obigen Beispiel aufbauen, da es sinnvoll ist, zu warten, bis ein Element gefunden wurde, bevor es sichtbar wird.

Nachfolgend finden Sie unsere erste Anwendung der Verkettung von Versprechen:

const button = driver.wait (
  until.elementLocated (By.id ('my-button')),
  20000
)
.then (element => {
   return driver.wait (
     until.elementIsVisible (Element),
     20000
   );
});
button.click ();

Wir könnten hier fast aufhören und du wärst schon viel besser dran. Mit dem obigen Code eliminieren Sie eine Vielzahl von Testfällen, die andernfalls nicht funktionieren würden, da ein Element nicht sofort im DOM vorhanden ist. Oder weil es aufgrund von Animationen nicht sofort sichtbar ist. Oder auch aus beiden Gründen.

Nachdem Sie die Technik verstanden haben, sollte es keinen Grund mehr geben, nicht deterministischen Selenium-Code zu schreiben. Das heißt nicht, dass dies immer einfach ist.

Wenn es schwieriger wird, geben Entwickler häufig auf und greifen auf driver.sleep zurück. Ich hoffe, dass ich Sie durch noch mehr Beispiele ermutigen kann, Ihre Tests deterministisch zu machen.

Schreiben Sie Ihre eigenen Bedingungen

Dank der until-Methode verfügt die JavaScript-Selenium-API bereits über einige praktische Methoden, die Sie mit driver.wait verwenden können. Sie können auch warten, bis ein Element nicht mehr vorhanden ist, bis ein Element einen bestimmten Text enthält, eine Warnung vorliegt oder viele andere Bedingungen vorliegen.

Wenn Sie in den bereitgestellten praktischen Methoden nicht finden, was Sie benötigen, müssen Sie Ihre eigenen Bedingungen aufschreiben. Das ist eigentlich ziemlich einfach, aber es ist schwierig, Beispiele zu finden. Und es gibt eine Sache, auf die wir noch eingehen werden.

Gemäß der Dokumentation können Sie driver.wait mit einer Funktion versehen, die true oder false zurückgibt.

Nehmen wir an, wir wollten warten, bis ein Element vollständig undurchsichtig ist:

// Hole das Element.
const element = driver.wait (
  until.elementLocated (By.id ('some-id')),
  20000
);
// driver.wait benötigt nur eine Funktion, die true oder false zurückgibt.
driver.wait (() => {
  return element.getCssValue ('opacity')
    .then (Opazität => Opazität === '1');
});

Das scheint nützlich und wiederverwendbar, also lassen Sie es uns in eine Funktion umwandeln:

const waitForOpacity = function (element) {
  Rückgabe driver.wait (element => element.getCssValue ('opacity')
    .then (Opazität => Opazität === '1');
  );
};

Und dann können wir unsere Funktion nutzen:

driver.wait (
  until.elementLocated (By.id ('some-id')),
  20000
)
.then (waitForOpacity);

Hier kommt der Gotcha. Was ist, wenn wir auf das Element klicken möchten, nachdem es die volle Deckkraft erreicht hat? Wenn wir versuchen, den von oben zurückgegebenen Wert zuzuweisen, erhalten wir nicht das, was wir wollen:

const element = driver.wait (
  until.elementLocated (By.id ('some-id')),
  20000
)
.then (waitForOpacity);
// Ups, Element ist wahr oder falsch, kein Element.
element.click ();

Aus dem gleichen Grund können wir auch keine Verkettung von Versprechen verwenden.

const element = driver.wait (
  until.elementLocated (By.id ('some-id')),
  20000
)
.then (waitForOpacity)
.then (element => {
  // Nein, Element ist auch hier ein Boolescher Wert.
  element.click ();
});

Dies ist leicht zu beheben. Hier ist unsere verbesserte Methode:

const waitForOpacity = function (element) {
  Rückgabe driver.wait (element => element.getCssValue ('opacity')
    .then (Opazität => {
      if (opacity === '1') {
        return element;
      } else {
        falsch zurückgeben;
    });
  );
};

Das obige Muster, das das Element zurückgibt, wenn die Bedingung wahr ist, und ansonsten false, ist ein wiederverwendbares Muster, das Sie zum Schreiben Ihrer eigenen Bedingungen verwenden können.

Hier ist, wie wir es mit Versprechen Verkettung verwenden können:

driver.wait (
  until.elementLocated (By.id ('some-id')),
  20000
)
.then (waitForOpacity)
.then (element => element.click ());

Oder auch:

const element = driver.wait (
  until.elementLocated (By.id ('some-id')),
  20000
)
.then (waitForOpacity);
element.click ();

Indem Sie Ihre eigenen einfachen Bedingungen schreiben, können Sie Ihre Optionen erweitern, um Ihre Tests deterministisch zu machen. Aber das ist nicht immer genug.

Negativ werden

Das ist richtig, manchmal muss man negativ statt positiv sein. Damit meine ich zu testen, ob etwas nicht mehr existiert oder nicht mehr sichtbar ist.

Angenommen, im DOM ist bereits ein Element vorhanden. Sie sollten jedoch erst damit interagieren, wenn einige Daten über AJAX geladen wurden. Das Element könnte mit einem "Laden ..." - Panel abgedeckt werden.

Wenn Sie genau auf die Bedingungen geachtet haben, die die until-Methode bietet, sind Ihnen möglicherweise Methoden wie elementIsNotVisible oder elementIsDisabled oder das nicht so offensichtliche stalenessOfElement aufgefallen.

Sie können testen, ob ein Fenster "Laden ..." nicht mehr sichtbar ist:

// Wurde bereits zum DOM hinzugefügt, daher wird dies sofort zurückgegeben.
const desiredElement = driver.wait (
  until.elementLocated (By.id ('some-id')),
  20000
);
// Aber das Element ist erst im Lade-Panel wirklich fertig
// ist weg.
driver.wait (
  until.elementIsNotVisible (By.id ('loading-panel')),
  20000
);
// Das Lade-Panel ist nicht mehr sichtbar und kann jetzt sicher interagiert werden.
wantedElement.click ();

Ich finde das stalenessOfElement besonders nützlich. Es wird gewartet, bis ein Element aus dem DOM entfernt wurde, was auch bei der Seitenaktualisierung passieren kann.

Hier ist ein Beispiel für das Warten auf die Aktualisierung des Inhalts eines Iframes, bevor Sie fortfahren:

let iframeElem = driver.wait (
  until.elementLocated (By.className ('result-iframe')),
  20000
);
// Jetzt machen wir etwas, das den iframe auffrischen lässt.
someElement.click ();
// Warten Sie, bis der vorherige Iframe nicht mehr existiert:
driver.wait (
  bis.
  20000
);
// Zum neuen iframe wechseln.
driver.wait (
  till.ableToSwitchToFrame (By.className ('result-iframe')),
  20000
);
// Jeder folgende Code ist relativ zum neuen iframe.

Sei immer deterministisch und schlafe nicht

Ich hoffe, diese Beispiele haben Ihnen geholfen, besser zu verstehen, wie Sie Ihre Selentests deterministisch machen können. Verlassen Sie sich nicht auf driver.sleep mit einer willkürlichen Vermutung.

Wenn Sie Fragen oder Ihre eigenen Techniken haben, um Selentests deterministisch zu machen, hinterlassen Sie bitte einen Kommentar.