Sådan skrives pålidelige browsertest ved hjælp af Selenium og Node.js

Der er mange gode artikler om, hvordan man kommer i gang med automatisk browsertest ved hjælp af NodeJS-versionen af ​​Selenium.

Nogle pakker testene i Mocha eller Jasmine, og nogle automatiserer alt med npm eller Grunt eller Gulp. Alle af dem beskriver, hvordan du installerer det, du har brug for, sammen med at give et grundlæggende eksempel på en arbejdskode. Dette er meget nyttigt, fordi det kan være en udfordring at få alle de forskellige brikker til at fungere for første gang.

Men de undgår at grave i detaljerne i de mange gotchas og bedste praksis med at automatisere din browsertest, når du bruger Selenium.

Denne artikel fortsætter, hvor de andre artikler går væk, og hjælper dig med at skrive automatiserede browsertest, der er langt mere pålidelige og vedligeholdelige med NodeJS Selenium API.

Undgå at sove

Selenium driver.sleep-metoden er din værste fjende. Og alle bruger det. Dette kan skyldes, at dokumentationen til Node.js-versionen af ​​Selenium er stram og kun dækker syntaks for API. Det mangler eksempler på det virkelige liv.

Eller det kan skyldes, at en masse eksempelkode i blogartikler og på spørgsmål og spørgsmål som StackOverflow bruger det.

Lad os sige, at et panel animerer fra en størrelse på nul til fuld størrelse. Lad os se.

Det sker så hurtigt, at du måske ikke bemærker, at knapperne og kontrollerne inde i panelet konstant ændrer størrelse og position.

Her er en langsom version. Vær opmærksom på den grønne luk-knap, og du kan se panelets skiftende størrelse og placering.

Dette er næppe et problem for rigtige brugere, fordi animationen sker så hurtigt. Hvis det var langsomt nok, som i den anden video, og du forsøgte manuelt at klikke på luk-knappen, mens dette skete, kan du klikke på den forkerte knap eller gå glip af knappen helt.

Men disse animationer sker normalt så hurtigt, du har aldrig en chance for at gøre det. Mennesker venter bare på, at animationen er færdig. Ikke sandt med Selen. Det er så hurtigt, at det kan prøve at klikke på elementer, der stadig animeres, og du får muligvis en fejlmeddelelse som:

System.InvalidOperationException: Elementet kan ikke klikkes på punktet (326, 792.5)

Dette er, når mange programmerere vil sige “Aha! Jeg bliver nødt til at vente på, at animationen er færdig, så jeg bruger bare driver.sleep (1000) til at vente på, at panelet skal være anvendeligt. ”

Så hvad er problemet?

Driver.sleep-erklæringen (1000) gør, hvordan det ser ud. Det stopper udførelsen af ​​dit program i 1000 millisekunder og giver browseren mulighed for at fortsætte med at arbejde. Lav layout, falmning eller animering af elementer, indlæsning af siden, eller hvad som helst.

Brug af eksemplet ovenfra, hvis panelet falmede over en periode på 800 millisekunder, vil driveren. Søvn (1000) normalt udføre det, du ønsker. Så hvorfor ikke bruge det?

Den vigtigste grund er, at den ikke er deterministisk. Betydning, at det kun fungerer noget af tiden. Da det kun fungerer noget af tiden, ender vi med skrøbelige tests, der bryder under visse betingelser. Dette giver automatiseret browser-test af et dårligt navn.

Hvorfor fungerer det kun noget af tiden? Med andre ord, hvorfor er det ikke deterministisk?

Det, du bemærker med dine øjne, er ikke ofte det eneste, der sker på et websted. En element fade-in eller animation er et perfekt eksempel. Vi skal ikke bemærke disse ting, hvis de gøres godt.

Hvis du beder Selenium om først at finde et element og derefter klikke på det, er der muligvis kun et par millisekunder mellem disse to operationer. Selen kan være langt hurtigere end et menneske.

Når et menneske bruger hjemmesiden, venter vi på, at elementet falmer ind, før det klikker på det. Og når denne fade-in tager mindre end et sekund, bemærker vi sandsynligvis ikke engang, at vi gør det ”venter”. Selen er ikke kun hurtigere og mindre tilgivende, dine automatiske test skal håndtere alle mulige andre uforudsigelige faktorer:

  1. Designeren af ​​din webside kan muligvis ændre animationstiden fra 800 millisekunder til 1200 millisekunder. Din test er lige knækket.
  2. Browsere gør ikke altid nøjagtigt, hvad du beder om. På grund af systembelastning kan animationen faktisk stoppe og tage længere end 800 millisekunder, måske endda længere end din søvn på 1000 millisekunder. Din test er lige knækket.
  3. Forskellige browsere har forskellige layoutmotorer og prioriterer layoutoperationerne forskelligt. Føj en ny browser til din testsuite, og dine test er lige gået.
  4. Browsere og JavaScript, der kontrollerer en side, er asynkrone af natur. Hvis animationen i vores eksempel ændrer funktionalitet, der har brug for information fra back-end, kan programmereren tilføje et AJAX-opkald og vente på resultatet, før animationen tændes.
    Vi beskæftiger os nu med netværkstidsforsinkelse og nul-garanti for, hvor lang tid det vil tage for panelet at vise. Din test er lige knækket.
  5. Der er helt sikkert andre grunde, som jeg ikke kender til.
    Selv en browser på egen hånd er et komplekst dyr, og alle har fejl. Så vi taler om at prøve at få den samme ting til at fungere over flere forskellige browsere, flere forskellige browserversioner, flere forskellige operativsystemer og flere forskellige operativsystemversioner.
    På et tidspunkt bryder dine test bare, hvis de ikke er deterministiske. Ikke underligt, at programmerere opgiver automatisk test af browseren og klager over, hvor skrøbelige testene er.

Hvad gør programmerere typisk for at ordne ting, når noget af ovenstående sker? De sporer ting tilbage til timingproblemer, så det åbenlyse svar er at øge tiden i driver.sleep-erklæringen. Kryds derefter fingrene, at det dækker alle mulige fremtidige scenarier for systembelastning, layoutmotorforskelle osv. Det er ikke deterministisk, og det går i stykker, så gør ikke dette!

Hvis du ikke er overbevist endnu, her er endnu en grund: dine test kører meget hurtigere. Animationen fra vores eksempel tager kun 800 millisekunder, håber vi. For at håndtere ”vi håber” og få testene til at fungere under alle forhold, vil du sandsynligvis se noget som driver.sleep (2000) i den virkelige verden.

Det er mere end et helt sekund tabt for kun et trin i dine automatiserede test. Over mange trin tilføjer det hurtigt. En nyligt refactored test for en af ​​vores websider, der tog flere minutter på grund af overforbrug af driver.sleep tager nu mindre end femten sekunder.

Det meste af resten af ​​denne artikel giver specifikke eksempler på, hvordan du kan foretage dine test fuldt deterministisk og undgå at bruge driver.sleep.

En note om løfter

JavaScript API til Selenium bruger kraftigt løfter, og det gør også et godt stykke arbejde at skjule det ved at bruge en indbygget løftemanager. Dette ændrer sig og udskrives.

I fremtiden bliver du enten nødt til at lære at bruge løftekæning selv, eller bruge de nye JavaScript-async-funktioner som venter.

I denne artikel gør eksemplerne stadig brug af den traditionelle indbyggede Selenium-løfter manager og drager fordel af løftekæden. Kodeeksemplerne her giver mere mening, hvis du forstår, hvordan løfter fungerer. Men du kan stadig komme meget ud af denne artikel, hvis du vil springe over læringsløfter for øjeblikket.

Lad os komme igang

Fortsætter med vores eksempel på en knap, som vi ønsker at klikke på et panel, der animerer, lad os se på flere specifikke gotchas, der kan bryde vores test.

Hvad med et element, der dynamisk er føjet til siden og ikke engang eksisterer endnu, når siden er færdig med indlæsning?

Venter på, at et element skal være til stede i DOM

Følgende kode fungerer ikke, hvis et element med et CSS-id for 'min-knap' blev føjet til DOM efter sidelastning:

// Seleninitialiseringskode blev udeladt for klarhed
// Indlæs siden.
driver.get ( 'https: /foobar.baz');
// Find elementet.
const-knap = driver.findElement (By.id ('min-knap'));
button.click ();

Driver.findElement-metoden forventer, at elementet allerede er til stede i DOM. Det fejler, hvis elementet ikke kan findes med det samme. I dette tilfælde betyder det øjeblikkeligt "efter sideindlæsningen er fuldført" på grund af den forudgående driver.get-erklæring.

Husk, at den aktuelle version af JavaScript Selenium styrer løftene for dig. Så hver erklæring vil være fuldstændig afsluttet, inden den går videre til den næste erklæring.

Bemærk: Ovenstående opførsel er ikke altid uønsket. driver.findElement på egen hånd kan faktisk være praktisk, hvis du er sikker på, at elementet allerede skal være der.

Lad os først se på den forkerte måde at løse dette på. Efter at have fået at vide, kan det tage et par sekunder, før elementet føjes til DOM:

driver.get ( 'https: /foobar.baz');
// Siden er indlæst, gå nu i dvale i et par sekunder.
driver.sleep (3000);
// Bed om, at tre sekunder er nok, og find elementet.
const-knap = driver.findElement (By.id ('min-knap'));
button.click ();

Af alle de tidligere nævnte grunde kan dette gå i stykker, og sandsynligvis vil det. Vi er nødt til at lære at vente på, at et element bliver placeret. Dette er temmelig let, og du ser det ofte i eksempler fra hele internettet. I eksemplet nedenfor bruger vi den veldokumenterede driver.wait-metode til at vente i op til tyve sekunder på elementet, der findes i DOM:

const-knap = driver.vente (
  until.elementLocated (By.id ( 'min-knappen')),
  20000
);
button.click ();

Der er umiddelbare fordele ved dette. For eksempel, hvis elementet føjes til DOM på et sekund, afsluttes metoden driver.wait på et sekund. Det vil ikke vente de fulde tyve sekunder.

På grund af denne opførsel kan vi sætte en masse polstring i vores timeout uden at bekymre os om, at timeout bremser vores tests. I modsætning til driveren. Sover, der altid venter hele den angivne tid.

Dette fungerer i mange tilfælde. Men et tilfælde, det ikke fungerer i, er at forsøge at klikke på et element, der findes i DOM, men som endnu ikke er synligt.

Selen er smart nok til ikke at klikke på et element, der ikke er synligt. Dette er godt, fordi brugere ikke kan klikke på usynlige elementer, men det gør os til at arbejde hårdere med at skabe pålidelige automatiserede test.

Venter, indtil et element er synligt

Vi vil bygge videre på ovenstående eksempel, fordi det giver mening at vente på, at et element bliver placeret, før det bliver synligt.

Du finder også vores første brug af løftekæde nedenfor:

const-knap = driver.vente (
  until.elementLocated (By.id ( 'min-knappen')),
  20000
)
.then (element => {
   returner driver.wait (
     until.elementIsVisible (element),
     20000
   );
});
button.click ();

Vi kunne næsten stoppe her, og du ville allerede være langt bedre stillet. Med koden ovenfor vil du eliminere belastninger af testtilfælde, der ellers ville gå i stykker, fordi et element ikke umiddelbart findes i DOM. Eller fordi det ikke umiddelbart er synligt på grund af ting som animation. Eller endda af begge grunde.

Nu hvor du forstår teknikken, skal der aldrig være en grund til at skrive selen-kode, der ikke er deterministisk. Det er ikke at sige, at dette altid er let.

Når tingene bliver vanskeligere, giver udviklere ofte op igen og tyr til driveren. Jeg håber, at ved at give endnu flere eksempler, kan jeg opfordre dig til at gøre dine prøver deterministiske.

Skrivning af dine egne betingelser

Takket være indtil-metoden har JavaScript Selenium API allerede en håndfuld bekvemmelighedsmetoder, du kan bruge med driver.wait. Du kan også vente, indtil et element ikke længere findes, på et element, der indeholder specifik tekst, med en alarm, der skal være til stede, eller mange andre betingelser.

Hvis du ikke kan finde det, du har brug for i de medfølgende bekvemmelighedsmetoder, skal du skrive dine egne betingelser. Dette er faktisk temmelig let, men det er svært at finde eksempler. Og der er en gotcha - som vi vil komme til.

I henhold til dokumentationen kan du give driver.wait en funktion, der returnerer sandt eller usant.

Lad os sige, at vi ønskede at vente på, at et element blev fuld opacitet:

// Hent elementet.
const element = driver.wait (
  until.elementLocated (By.id ( 'nogle-id')),
  20000
);
// driver.wait har bare brug for en funktion, der returnerer true for false.
driver.wait (() => {
  return element.getCssValue ('opacitet')
    .then (opacity => opacity === '1');
});

Det virker nyttigt og genanvendeligt, så lad os sætte det i en funktion:

const waitForOpacity = funktion (element) {
  returner driver.wait (element => element.getCssValue ('opacitet')
    .then (opacity => opacity === '1');
  );
};

Og så kan vi bruge vores funktion:

driver.wait (
  until.elementLocated (By.id ( 'nogle-id')),
  20000
)
Ringing (waitForOpacity);

Her kommer gotcha. Hvad hvis vi vil klikke på elementet, når det har nået fuld opacitet? Hvis vi prøver at tildele den værdi, der er returneret af ovenstående, ville vi ikke få det, vi ønsker:

const element = driver.wait (
  until.elementLocated (By.id ( 'nogle-id')),
  20000
)
Ringing (waitForOpacity);
// Ups, element er sandt eller falsk, ikke et element.
element.click ();

Vi kan heller ikke bruge løftekæde af samme grund.

const element = driver.wait (
  until.elementLocated (By.id ( 'nogle-id')),
  20000
)
Ringing (waitForOpacity)
.then (element => {
  // Nej, element er også en boolsk her.
  element.click ();
});

Dette er let at løse. Her er vores forbedrede metode:

const waitForOpacity = funktion (element) {
  returner driver.wait (element => element.getCssValue ('opacitet')
    .then (opacitet => {
      if (uklarhed === '1') {
        returelement;
      } andet {
        vende tilbage falsk;
    });
  );
};

Ovenstående mønster, der returnerer elementet, når betingelsen er sand, og returnerer falsk ellers, er et genanvendeligt mønster, du kan bruge, når du skriver dine egne betingelser.

Sådan kan vi bruge det med løftekæde:

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

Eller endda:

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

Ved at skrive dine egne enkle betingelser kan du udvide dine muligheder for at gøre dine test deterministiske. Men det er ikke altid nok.

Gå negativ

Det er rigtigt, nogle gange skal du være negativ i stedet for positiv. Det, jeg mener med dette, er at teste for, at noget ikke længere eksisterer, eller for, at noget ikke længere er synligt.

Lad os sige, at der allerede findes et element i DOM, men du skal ikke interagere med det, før nogle data er indlæst via AJAX. Elementet kunne være dækket med et "indlæsning ..." -panel.

Hvis du fulgte nøje opmærksomhed med betingelserne, der blev tilbudt med indtil metoden, har du måske bemærket metoder som elementIsNotVisible eller elementIsDisabled eller den ikke så åbenlyse stilhedOfElement.

Du kan teste, om et "indlæser ..." -panel ikke længere er synligt:

// Allerede føjet til DOM, så dette vil straks vende tilbage.
const wantedElement = driver.wait (
  until.elementLocated (By.id ( 'nogle-id')),
  20000
);
// Men elementet er ikke rigtig klar før indlæsningspanelet
// er væk.
driver.wait (
  until.elementIsNotVisible (By.id (loading-panel «)),
  20000
);
// Indlæsningspanelet er ikke længere synligt, sikkert at interagere nu.
desiredElement.click ();

Jeg finder, at stalenessOfElement er særlig nyttigt. Det venter, indtil et element er blevet fjernet fra DOM, hvilket også kan ske fra sideopdatering.

Her er et eksempel på at vente på, at indholdet af en iframe skal opdateres, før du fortsætter:

lad iframeElem = driver.wait (
  until.elementLocated (By.className ( 'resultat-iframe')),
  20000
);
// Nu gør vi noget der får iframe til at opdatere.
someElement.click ();
// Vent på, at den forrige iframe ikke længere findes:
driver.wait (
  until.stalenessOf (iframeElem),
  20000
);
// Skift til den nye iframe.
driver.wait (
  until.ableToSwitchToFrame (By.className ( 'resultat-iframe')),
  20000
);
// Enhver følgende kode vil være i forhold til den nye iframe.

Vær altid deterministisk, og sov ikke

Jeg håber, at disse eksempler har hjulpet dig med bedre at forstå, hvordan du gør dine Selenium-test deterministiske. Stol ikke på føreren. Sover med en vilkårlig gæt.

Hvis du har spørgsmål eller dine egne teknikker til at gøre selen-test deterministisk, bedes du efterlade en kommentar.