Sådan skriver du pålidelige browsertests ved hjælp af Selenium og Node.js

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

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

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

Denne artikel fortsætter, hvor de andre artikler holder op, og vil hjælpe dig med at skrive automatiserede browsertests, der er langt mere pålidelige og vedligeholdelige med NodeJS Selenium API.

Undgå at sove

Selen- driver.sleepmetoden er din værste fjende. Og alle bruger det. Dette kan skyldes, at dokumentationen til Node.js-versionen af ​​Selenium er kortfattet og kun dækker syntaksen for API. Det mangler eksempler fra det virkelige liv.

Eller det kan skyldes, at mange eksempler på kode i blogartikler og på Q & A-websteder som StackOverflow gør brug af det.

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

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

Her er en langsommere 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 nogensinde et problem for rigtige brugere, fordi animationen sker så hurtigt. Hvis det var langsomt nok, som i den anden video, og du forsøgte at manuelt klikke på knappen Luk, mens dette skete, kan du klikke på den forkerte knap eller gå glip af knappen helt.

Men disse animationer sker normalt så hurtigt, at du aldrig har 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 : Element is not clickable at point (326, 792.5)

Dette er, når mange programmører 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 kan bruges. ”

Så hvad er problemet?

Det driver.sleep(1000)udsagn gør hvad det ligner. Det stopper udførelsen af ​​dit program i 1000 millisekunder og giver browseren mulighed for at fortsætte med at arbejde. Gør layout, falmning eller animering af elementer, indlæsning af siden eller hvad som helst.

Ved hjælp af eksemplet ovenfra, hvis panelet falmede ind over en periode på 800 millisekunder, driver.sleep(1000)ville det normalt opnå det, du vil have. Så hvorfor ikke bruge det?

Den vigtigste årsag er, at den ikke er deterministisk. Det betyder, at det kun fungerer noget af tiden. Da det kun fungerer noget af tiden, ender vi med skrøbelige tests, der går i stykker under visse betingelser. Dette giver automatisk browsertest et dårligt navn.

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

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

Hvis du beder Selen om først at finde et element og derefter klikke på det, er der muligvis kun få 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, inden vi klikker på det. Og når denne udtoning tager mindre end et sekund, bemærker vi sandsynligvis ikke engang, at vi gør det "ventende". Selen er ikke kun hurtigere og mindre tilgivende, dine automatiske tests skal håndtere alle mulige andre uforudsigelige faktorer:

  1. Designeren af ​​din webside ændrer muligvis animationstiden fra 800 millisekunder til 1200 millisekunder. Din test brød lige.
  2. Browsere gør ikke altid nøjagtigt, hvad du beder om. På grund af systembelastning kan animationen faktisk gå i stå og tage længere tid end 800 millisekunder, måske endda længere end din søvn på 1000 millisekunder. Din test brød lige .
  3. Forskellige browsere har forskellige layoutmotorer og prioriterer layoutoperationerne forskelligt. Tilføj en ny browser til din testpakke, og dine tests er lige gået .
  4. Browsere og JavaScript, der styrer en side, er asynkrone af natur. Hvis animationen i vores eksempel ændrer funktionalitet, der har brug for information fra back-end, kan programmøren muligvis tilføje et AJAX-opkald og vente på resultatet, før animationen affyres.

    Vi har nu at gøre med netværkslatens og nul garanti for, hvor lang tid det tager for panelet at vise. Din test brød lige .

  5. Der er helt sikkert andre grunde, jeg ikke kender til.

    Selv en browser alene er et komplekst udyr, og alle har fejl. Så vi taler om at forsøge at få den samme ting til at fungere over flereforskellige browsere, flere forskellige browserversioner, flere forskellige operativsystemer og flere forskellige operativsystemversioner.

    På et tidspunkt går dine tests bare i stykker, hvis de ikke er deterministiske. Ikke underligt, at programmører giver op til automatisk browsertest og klager over, hvor skrøbelige testene er.

Hvad gør programmører typisk for at rette ting, når noget af ovenstående sker? De sporer tingene tilbage til timingproblemer, så det oplagte svar er at øge tiden i chaufføren. Søvnerklæring. Kryds derefter fingrene for, 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 det ikke!

Hvis du endnu ikke er overbevist, er der en grund til: dine tests kører meget hurtigere. Animationen fra vores eksempel tager kun 800 millisekunder, håber vi. For at håndtere det “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 tabttil kun et trin i dine automatiserede tests. Over mange trin tilføjes det hurtigt. En test, der for nylig blev ombygget til en af ​​vores websider, tog flere minutter på grund af overforbrug af chaufføren. Søvn tager nu mindre end femten sekunder.

Det meste af resten af ​​denne artikel giver specifikke eksempler på, hvordan du kan gøre dine test fuldt deterministiske og undgå brugen af driver.sleep.

En note om løfter

JavaScript API til Selen bruger kraftigt løfter, og det gør det også godt at skjule det ved hjælp af en indbygget løftehåndtering. Dette ændrer sig og vil blive udfaset.

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

In this article, the examples still make use of the traditional built-in Selenium promise manager and take advantage of promise chaining. The code examples here will make more sense if you understand how promises work. But you can still get a lot out of this article if you want to skip learning promises for the moment.

Let’s get started

Continuing with our example of a button that we want to click on a panel that animates, let’s look at several specific gotchas that could break our tests.

How about an element that is dynamically added to the page and does not even exist yet after the page is finished loading?

Waiting for an element to be present in the DOM

The following code would not work if an element with a CSS id of ‘my-button’ was added to the DOM after page load:

// Selenium initialization code left out for clarity
// Load the page.driver.get('https:/foobar.baz');
// Find the element.const button = driver.findElement(By.id('my-button'));
button.click();

The driver.findElement method expects the element to already be present in the DOM. It will error out if the element cannot be found immediately. In this case, immediately means “after page load is complete” due to the prior driver.get statement.

Remember that the current version of JavaScript Selenium manages the promises for you. So each statement will fully complete before moving on to the next statement.

Note: The above behavior isn’t always undesirable. driver.findElement on its own might be actually be handy if you are sure the element should already be there.

First let’s look at the wrong way of fixing this. Having been told it might take a few seconds for the element to be added to the DOM:

driver.get('https:/foobar.baz');
// Page has been loaded, now go to sleep for a few seconds.driver.sleep(3000);
// Pray that three seconds is enough and find the element.const button = driver.findElement(By.id('my-button'));
button.click();

For all the reasons mentioned earlier, this can break, and probably will. We need to learn how to wait for an element to be located. This is fairly easy, and you’ll see this often in examples from around the web. In the example below, we use the well documented driver.wait method to wait for up to twenty seconds for the element to be found in the DOM:

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

There are immediate advantages to this. For example, if the element is added to the DOM in one second, the driver.wait method will complete in one second. It will not wait the full twenty seconds specified.

Because of this behavior, we can put loads of padding in our timeout without worrying about the timeout slowing down our tests. Unlike the driver.sleep which will always wait the entire time specified.

This works in a lot of cases. But one case it doesn’t work in is trying to click an element that is present in the DOM, but is not yet visible.

Selenium is smart enough to not click an element that is not visible. This is good, because users cannot click invisible elements, but it does make us work harder at creating reliable automated tests.

Waiting until an element is visible

We will build on the above example because it makes sense to wait for an element to be located before it becomes visible.

You’ll also find our first use of promise chaining below:

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

We could almost stop here and you would already be far better off. With the code above, you will eliminate loads of test cases that would otherwise break because an element is not immediately present in the DOM. Or because it is not immediately visible due to things like animation. Or even for both reasons.

Now that you understand the technique, there should never be a reason to write Selenium code that is not deterministic. That’s not to say this is always easy.

When things become more difficult, developers often give up again and resort to driver.sleep. I hope by giving even more examples, I can encourage you to make your tests deterministic.

Writing your own conditions

Thanks to the until method, the JavaScript Selenium API already has a handful of convenience methods you can use with driver.wait. You can also wait until an element no longer exists, for an element that contains specific text, for an alert to be present, or many other conditions.

If you can’t find what you need in the supplied convenience methods you will need to write your own conditions. This is actually pretty easy, but it’s hard to find examples. And there is one gotcha — which we will get to.

According to the documentation, you can provide driver.wait with a function that returns true or false.

Let’s say we wanted to wait for an element to be full opacity:

// Get the element.const element = driver.wait( until.elementLocated(By.id('some-id')), 20000);
// driver.wait just needs a function that returns true of false.driver.wait(() => { return element.getCssValue('opacity') .then(opacity => opacity === '1');});

That seems useful and reusable, so let’s put it in a function:

const waitForOpacity = function(element) { return driver.wait(element => element.getCssValue('opacity') .then(opacity => opacity === '1'); );};

And then we can use our function:

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

Here comes the gotcha. What if we want to click the element after it reaches full opacity? If we try to assign the value returned by the above, we would not get what we want:

const element = driver.wait( until.elementLocated(By.id('some-id')), 20000).then(waitForOpacity);
// Oops, element is true or false, not an element.element.click();

We cannot use promise chaining either, for the same reason.

const element = driver.wait( until.elementLocated(By.id('some-id')), 20000).then(waitForOpacity).then(element => { // Nope, element is a boolean here too. element.click();}); 

This is easy to fix. Here is our improved method:

const waitForOpacity = function(element) { return driver.wait(element => element.getCssValue('opacity') .then(opacity => { if (opacity === '1') { return element; } else { return false; }); );};

The above pattern, which returns the element when the condition is true, and returns false otherwise, is a reusable pattern you can use when writing your own conditions.

Here is how we can use it with promise chaining:

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

Or even:

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

By writing your own simple conditions, you can expand your options for making your tests deterministic. But that’s not always enough.

Go negative

That’s right, sometimes you need to be negative instead of positive. What I mean by this is to test for something to no longer exist or for something to not be visible anymore.

Let’s say an element exists in the DOM already, but you shouldn’t interact with it until some data is loaded via AJAX. The element could be covered with a “loading…” panel.

If you paid close attention to the conditions offered by the until method, you might have noticed methods like elementIsNotVisible or elementIsDisabled or the not so obvious stalenessOfElement.

You could test for a “loading…” panel to no longer be visible:

// Already added to the DOM, so this will return immediately.const desiredElement = driver.wait( until.elementLocated(By.id('some-id')), 20000);
// But the element isn't really ready until the loading panel// is gone.driver.wait( until.elementIsNotVisible(By.id('loading-panel')), 20000);
// Loading panel is no longer visible, safe to interact now.desiredElement.click();

I find the stalenessOfElement to be particularly useful. It waits until an element has been removed from the DOM, which could also happen from page refresh.

Here is an example of waiting for the contents of an iframe to refresh before continuing:

let iframeElem = driver.wait( until.elementLocated(By.className('result-iframe')), 20000 );
// Now we do something that causes the iframe to refresh.someElement.click();
// Wait for the previous iframe to no longer exist:driver.wait( until.stalenessOf(iframeElem), 20000);
// Switch to the new iframe. driver.wait( until.ableToSwitchToFrame(By.className('result-iframe')), 20000);
// Any following code will be relative to the new iframe.

Always be deterministic, and don’t sleep

Jeg håber, at disse eksempler har hjulpet dig bedre at forstå, hvordan du gør dine Selen-test deterministiske. Stol ikke på driver.sleepmed et vilkårligt gæt.

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