Tips för att undvika spröda UI-test

I den senaste artikeln pratade jag om några idéer och mönster, som Page Object-mönstret, som hjälper till att skriva underhållbara gränssnittstester. I den här artikeln kommer vi att diskutera några avancerade ämnen som kan hjälpa dig att skriva mer robusta tester och felsöka dem när de misslyckas:

  • Vi diskuterar varför att lägga fasta förseningar i användarprov är en dålig idé och hur du kan bli av med dem.
  • Webbläsarautomatiseringsramar inriktar UI-element med hjälp av selektorer och det är mycket viktigt att använda bra selektorer för att undvika spröda test. Så jag ger dig lite råd om att välja rätt väljare och inriktningselement direkt när det är möjligt.
  • UI-testen misslyckas oftare än andra typer av test, så hur kan vi felsöka ett brutet UI-test och ta reda på vad som orsakade felet? I det här avsnittet visar jag dig hur du kan ta en skärmdump och sidans HTML-källa när ett gränssnittstest misslyckas så att du kan undersöka det enklare.

Jag ska använda Selen för de webbläsarautomationsämnen som diskuteras i den här artikeln.

Liksom den föregående artikeln gäller de begrepp och lösningar som diskuteras i denna artikel oavsett vilket språk och användargränssnitt du använder. Innan du går vidare, läs den föregående artikeln som jag kommer att referera till den och dess provkod några gånger. Oroa dig inte; jag väntar här.


Lägg inte till förseningar till dina test

tillsats Thread.sleep (eller allmänt förseningar) känns som en oundviklig hack när det gäller gränssnittstestning. Du har ett test som misslyckas periodiskt och efter en undersökning spårar du det tillbaka till tillfälliga förseningar i svaret. Till exempel navigerar du till en sida och ser eller hävdar något innan sidan är fulladdat och din webbläsarautomatisering ramar ett undantag som indikerar att elementet inte existerar. En hel del saker kan bidra till denna försening. Till exempel:

  • Webbservern, databasen och / eller nätverket är överbelastade och upptagna med andra förfrågningar.
  • Sidan som testas är långsam eftersom den laddar mycket data och / eller frågar många bord.
  • Du väntar på att en händelse inträffar på sidan som tar tid.

Eller en blandning av dessa och andra frågor.

Låt oss säga att du har en sida som normalt tar mindre än en sekund att ladda, men testen som slår på det misslyckas då och då på grund av tillfällig fördröjning som svar. Du har några alternativ:

  • Du lägger inte till en fördröjning: i det här fallet kommer testen som träffar den sidan att misslyckas vilket minskar ditt förtroende för testen.
  • Du lägger till en sekunders fördröjning till testen som träffar den här sidan: i det här fallet Allt av dessa test är alltid kommer att ta en sekund längre, även när sidan laddas snabbt, men även då kan testen inte garanteras att passera eftersom sidan kanske tar längre tid än en sekund att ladda.
  • Du kanske bestämmer dig för att lägga till några sekunders fördröjning: det här säkerställer att sidan alltid laddas, men nu tar din UI-test längre och längre.

Du ser, det finns ingen vinst med godtyckliga förseningar: du får antingen en långsam eller en spröd testpaket. Här kommer jag att visa dig hur man undviker att införa fasta förseningar i dina test. Vi kommer att diskutera två typer av förseningar som borde omfatta ganska mycket alla fall du måste hantera: lägga till en global fördröjning och vänta på att något ska hända.

Lägga till en global fördröjning

Om alla dina sidor tar ungefär samma tid för att ladda, vilket är längre än väntat, kommer de flesta testerna att misslyckas på grund av ett oändligt svar. I sådana fall kan du använda Implicit Waits:

En implicit vänta är att berätta för WebDriver att pollen DOM under en viss tid när man försöker hitta ett element eller element om de inte är omedelbart tillgängliga. Standardinställningen är 0. När den är inställd, är den implicita väntetiden inställd för livet för WebDriver-objektet.

Så här ställer du in en implicit vänta:

WebDriver driver = ny FirefoxDriver (); .. Driver.Manage () Tidsgränser () ImplicitlyWait (TimeSpan.FromSeconds (5));

På det här sättet berättar du att Selen ska vänta upp till 5 sekunder när det försöker hitta ett element eller interagera med sidan. Så nu kan du skriva:

driver.Url = "http: // somedomain / url_that_delays_loading"; IWebElement myDynamicElement = driver.FindElement (By.Id ("someDynamicElement"));

istället för:

driver.Url = "http: // somedomain / url_that_delays_loading"; Thread.sleep (5000); IWebElement myDynamicElement = driver.FindElement (By.Id ("someDynamicElement"));

Fördelen med detta tillvägagångssätt är det FindElement kommer att återvända så snart den hittar elementet och väntar inte på hela 5 sekunder när elementet är tillgängligt tidigare.

När en imponerande väntan är inställd på din WebDriver Exempel gäller det för alla handlingar på föraren; så du kan bli av med många Thread.sleeps i din kod.

5 sekunder är en väntan jag gjorde upp för den här artikeln - du borde hitta den optimala implicita väntetiden för din ansökan och du borde göra denna vänta så kort som möjligt. Från API-dokumentationen:

Ökad implisitt väntetid bör användas med god tro eftersom det kommer att ha en negativ inverkan på testkörningstiden, speciellt när den används med långsammare platsstrategier som XPath.

Även om du inte använder XPath, använder du långa implicit väntar saktar dina test, särskilt när vissa tester verkligen misslyckas, eftersom webdrivrutinen kommer att vänta länge innan det går ut och kastar ett undantag.

Väntar på exakta händelser / ändringar

Att använda imponerande vänta är ett bra sätt att bli av med många hårdkodade förseningar i din kod. men du kommer fortfarande att hitta dig själv i en situation där du måste lägga till några fasta förseningar i din kod eftersom du väntar på att något ska hända: en sida är långsammare än alla andra sidor och du måste vänta längre, du är väntar på ett AJAX-samtal för att slutföra eller för att ett element ska visas på eller försvinna från sidan etc. Det är här du behöver uttryckliga väntar.

Explicit vänta

Så du har satt den implicita väntan till 5 sekunder och det fungerar för många av dina test. men det finns fortfarande några sidor som ibland tar mer än 5 sekunder att ladda och resultera i misslyckade test.

Som en sidnot ska du undersöka varför en sida tar så lång tid innan du försöker fixa det brutna testet genom att vänta det längre. Det kan finnas en prestandafråga på sidan som leder till det röda testet, i vilket fall du ska fixa sidan, inte testet.

Vid en långsam sida kan du ersätta fasta förseningar med Explicit Waits:

Ett uttryckligt väntar är kod du definierat för att vänta på att ett visst tillstånd inträffar innan du fortsätter längre i koden.

Du kan ansöka uttryckliga väntar med WebDriverWait klass. WebDriverWait bor i WebDriver.Support montering och kan installeras med Selenium.Support nuget:

///  /// Ger möjlighet att vänta på ett godtyckligt tillstånd under testkörning. ///  public class WebDriverWait: DefaultWait ///  /// Initialiserar en ny instans av  klass. ///  /// WebDriver-förekomsten väntade.Timeout-värdet anger hur länge man ska vänta på tillståndet. offentlig WebDriverWait (IWebDriver-drivrutin, TimeSpan timeout); ///  /// Initialiserar en ny instans av  klass. ///  /// Ett objekt som implementerar  gränssnitt som används för att bestämma när tiden har gått.WebDriver-förekomsten väntade.Timeout-värdet anger hur länge man ska vänta på tillståndet.en  värde som anger hur ofta man ska kontrollera att tillståndet är sant. allmän WebDriverWait (IClock klocka, IWebDriver-drivrutin, TimeSpan timeout, TimeSpan sleepInterval); 

Här är ett exempel på hur du kan använda WebDriverWait i dina tester:

driver.Url = "http: // somedomain / url_that_takes_a_long_time_to_load"; WebDriverWait wait = nya WebDriverWait (drivrutin, TimeSpan.FromSeconds (10)); var myDynamicElement = wait.Until (d => d.FindElement (By.Id ("someElement")));

Vi berättar för Selen att vi vill att den ska vänta på den här sidan / elementet i upp till 10 sekunder.

Du kommer sannolikt att ha några sidor som tar längre tid än din standard implicita vänta och det är inte en bra kodningspraxis för att fortsätta att upprepa denna kod överallt. Trots allt Testkod är kod. Du kan istället extrahera detta till en metod och använda den från dina tester:

offentliga IWebElement FindElementWithWait (Genom, int secondsToWait = 10) var wait = nya WebDriverWait (WebDriver, TimeSpan.FromSeconds (secondsToWait)); återgå till väntan. Fram till (d => d.FindElement (by)); 

Då kan du använda den här metoden som:

var slowPage = ny slow slowdown ("http: // somedomain / url_that_takes_a_long_time_to_load"); var element = slowPage.FindElementWithWait (By.Id ("someElement"));

Detta är ett konstruerat exempel för att visa hur metoden kan se ut och hur den kan användas. Helst skulle du flytta alla sidans interaktioner till dina sidobjekt.

Alternativt uttryckligt väntan exempel

Låt oss se ett annat exempel på en uttrycklig väntan. Ibland är sidan laddad fullt men elementet finns inte kvar eftersom det senare laddas som ett resultat av en AJAX-förfrågan. Kanske är det inte ett element du väntar på men vill bara vänta på en AJAX-interaktion för att avsluta innan du kan göra ett påstående, säg i databasen. Återigen är det här de flesta utvecklare använder Thread.sleep för att se till att till exempel det AJAX-samtalet är gjort och posten nu finns i databasen innan de går vidare till nästa raden av testet. Detta kan enkelt åtgärdas med hjälp av JavaScript-körning!

I de flesta webbläsarautomatiseringsramar kan du köra JavaScript på den aktiva sessionen, och Selen är inget undantag. I Selen finns ett gränssnitt kallat IJavaScriptExecutor med två metoder:

///  /// Definierar gränssnittet genom vilket användaren kan utföra JavaScript. ///  offentligt gränssnitt IJavaScriptExecutor ///  /// Utför JavaScript i samband med den aktuella valda ramen eller fönstret. ///  /// JavaScript-koden som ska utföras. ///  /// Värdet som returneras av manuset. ///  objekt ExecuteScript (strängskript, paramsobjekt [] args); ///  /// Utför JavaScript som synkront i samband med den aktuella valda ramen eller fönstret. ///  /// JavaScript-koden som ska utföras. ///  /// Värdet som returneras av manuset. ///  objekt ExecuteAsyncScript (strängskript, paramsobjekt [] args); 

Detta gränssnitt är implementerat av RemoteWebDriver vilken är basklassen för alla webbdrivrutinsimplementeringar. Så på din webbdrivrutinsinstans kan du ringa ExecuteScript att köra ett JavaScript-skript. Här är en metod som du kan använda för att vänta på att alla AJAX-samtal ska slutföras (förutsatt att du använder jQuery):

// Det antas att man bor i en klass som har tillgång till den aktiva "WebDriver" -exemplen via "WebDriver" -fältet / -egenskapen. public void WaitForAjax (int secondsToWait = 10) var wait = nya WebDriverWait (WebDriver, TimeSpan.FromSeconds (secondsToWait)); wait.Until (d => (bool) ((IJavaScriptExecutor) d). ExecuteScript ("returnera jQuery.active == 0")); 

Kombinera ExecuteScript med WebDriverWait och du kan bli av med Thread.sleep läggas till för AJAX-samtal.

jQuery.active returnerar antalet aktiva AJAX-samtal initierad av jQuery; så när det är noll finns inga AJAX-samtal pågår. Denna metod fungerar uppenbart endast om alla AJAX-förfrågningar initieras av jQuery. Om du använder andra JavaScript-bibliotek för AJAX-kommunikation bör du konsultera API-dokumentationen för en likvärdig metod eller hålla reda på AJAX-samtal själv.

ExpectedCondition

Med uttrycklig väntetid kan du ställa in ett villkor och vänta tills det är uppfyllt eller att tidsgränsen löper ut. Vi såg hur vi kunde kontrollera för AJAX-samtal till slut - ett annat exempel är att kontrollera om ett element är synligt. Precis som AJAX-kontrollen, kan du skriva ett villkor som kontrollerar synligheten hos ett element. men det finns en enklare lösning för den som kallas ExpectedCondition.

Från Selen dokumentation:

Det finns några vanliga förhållanden som ofta möter när du automatiserar webbläsare.

Om du använder Java har du tur eftersom ExpectedCondition klass i Java är ganska omfattande och har många praktiska metoder. Du hittar dokumentationen här.

.Nätutvecklare är inte så lyckliga. Det finns fortfarande en ExpectedConditions klass i WebDriver.Support montering (dokumenterad här) men det är väldigt minimal:

offentligt förseglad klass ExpectedConditions ///  /// En förväntan för att kontrollera titeln på en sida. ///  /// Den förväntade titeln, som måste vara en exakt matchning. ///  ///  när titeln matchar annat, . ///  offentliga statiska Func TitleIs (strängtitel); ///  /// En förväntan om att kontrollera att rubriken på en sida innehåller en skiftlägeskänslig substring. ///  /// Titelfragmentet förväntas. ///  ///  när titeln matchar annat, . ///  offentliga statiska Func TitleContains (strängtitel); ///  /// En förväntan att kontrollera att ett element finns på DOM på en ///-sida. Detta betyder inte nödvändigtvis att elementet är synligt. ///  /// Locatorn brukade hitta elementet. ///  /// The  en gång den är belägen. ///  offentliga statiska Func ElementExists (By Locator); ///  /// En förväntan för att kontrollera att ett element är närvarande på DOM på en sida /// och synligt. Synlighet betyder att elementet inte bara visas men /// har också en höjd och bredd som är större än 0. ///  /// Locatorn brukade hitta elementet. ///  /// The  när den är placerad och synlig. ///  offentliga statiska Func ElementIsVisible (By Locator); 

Du kan använda denna klass i kombination med WebDriverWait:

var vänta = ny WebDriverWait (drivrutin, TimeSpan.FromSeconds (3)) var element = wait.Until (ExpectedConditions.ElementExists (By.Id ("foo")));

Som du kan se från klassens signatur ovan kan du kolla efter titeln eller delar av den och för existens och synlighet av element med ExpectedCondition. Utanför lådans stöd i. Net kan vara mycket minimal; men den här klassen är inget annat än en omslag runt några enkla förhållanden. Du kan lika enkelt genomföra andra vanliga förhållanden i en klass och använda den med WebDriverWait från dina testskript.

FluentWait

En annan pärla bara för Java-utvecklare är FluentWait. Från dokumentationssidan, FluentWait är

En implementering av Wait-gränssnittet som kan ha sin timeout och pollingintervall konfigurerad i flygningen. Varje FluentWait-instans definierar maximal tid för att vänta på ett tillstånd, liksom hur ofta man ska kontrollera tillståndet. Vidare kan användaren konfigurera väntetiden för att ignorera specifika typer av undantag medan man väntar, till exempel NoSuchElementExceptions när man söker efter ett element på sidan.

I följande exempel försöker vi hitta ett element med ID foo På sidan pollar var femte sekund i upp till 30 sekunder:

// Väntar 30 sekunder för att ett element ska vara närvarande på sidan, kontrollera // för närvaro en gång vart femte sekund. Vänta vänta = ny FluentWait(förare) .withTimeout (30, SECONDS) .PollingEvery (5, SECONDS) .ignoring (NoSuchElementException.class); WebElement foo = wait.until (ny funktion() public WebElement gäller (WebDriver-drivrutin) return driver.findElement (By.id ("foo")); );

Det finns två utmärkta saker om FluentWait: För det första kan du ange pollingintervallet som kan förbättra testprestanda och för det andra tillåter du att ignorera de undantag du inte är intresserad av.

FluentWait är ganska häftigt och det skulle vara coolt om en ekvivalent fanns i. Net too. Som sagt är det inte så svårt att genomföra det med WebDriverWait.


Välj rätt väljare

Du har dina sidobjekt på plats, har en bra DRY-underhållbar testkod och undviker också fasta förseningar i dina test. men dina test misslyckas fortfarande!

Användargränssnittet är vanligtvis den mest ändrade delen av en vanlig applikation: ibland flyttar du element runt på en sida för att ändra utformningen av sidan och ibland förändringar i sidstrukturen baserat på krav. Dessa förändringar på sidlayout och design kan leda till många brutna test om du inte väljer dina selektörer klokt.

Använd inte fuzzy selectors och lita inte på strukturen på din sida.

Många gånger har jag blivit ombedd om det är ok att lägga till ett ID på element på sidan endast för testning, och svaret är en rungande ja. För att göra vår kodenhet testbar gör vi många förändringar i det som att lägga till gränssnitt och använda beroendeinsprutning. Testkod är kod. Gör vad som krävs för att stödja dina test.

Låt oss säga att vi har en sida med följande lista:

  • Det bästa av män på jobbet
  • För dem som vill rocka, hälsar vi dig
  • Låt det finnas sten

I en av mina test vill jag klicka på "Let There Be Rock" -albumet. Jag skulle fråga om problem om jag använde följande väljare:

By.XPath ( "// ul [@ id = 'album-lista'] / li [3] / a")

När det är möjligt bör du lägga till ID på element och rikta dem direkt och utan att förlita sig på deras omgivande element. Så jag ska göra en liten ändring i listan:

  • Det bästa av män på jobbet
  • För dem som vill rocka, hälsar vi dig
  • Låt det finnas sten

Jag har lagt till id attribut till ankare baserat på det unika albumets id så att vi kan rikta en länk direkt utan att behöva gå igenom ul och li element. Så nu kan jag ersätta den spröda väljaren med By.Id ( "album-35") vilket är garanterat att fungera så länge som det här albumet finns på sidan, vilket för övrigt är en bra påstående också. För att skapa den väljaren skulle jag självklart behöva få tillgång till albumet ID från testkoden.

Det är inte alltid möjligt att lägga till unika ids för element, men som rader i ett rutnät eller element i en lista. I sådana fall kan du använda CSS-klasser och HTML-dataattribut för att bifoga spårbara egenskaper till dina element för enklare urval. Om du till exempel hade två listor med album på din sida, en som ett resultat av användarsökning och en annan för förslag till album baserat på användarens tidigare inköp, kan du skilja dem med en CSS-klass på ul element, även om den klassen inte används för att stylera listan:

Om du föredrar att inte ha oanvända CSS-klasser kan du istället använda HTML-dataattribut och ändra listorna till:

och:


Debugging UI Tests

En av huvudorsakerna till UI-testen misslyckas är att ett element eller en text inte finns på sidan. Ibland händer detta eftersom du landar på en fel sida på grund av navigeringsfel eller ändringar i sidnavigeringar på din webbplats eller valideringsfel. Andra gånger kan det vara på grund av en saknad sida eller ett serverfel.

Oavsett vad som orsakar felet och om du får det här på din CI-serverns logg eller i skrivbordskonsolen, a NoSuchElementException (eller liknande) är inte riktigt användbart för att räkna ut vad som gick fel, är det? Så när ditt test misslyckas är det enda sättet att felsöka felet att köra det igen och titta på det som det misslyckas. Det finns några knep som kan spara dig från att köra dina långsamma UI-tester för att felsöka. En lösning på detta är att fånga en skärmdump när ett test misslyckas så vi kan referera till det senare.

Det finns ett gränssnitt i Selen som heter ITakesScreenshot:

///  /// Definierar gränssnittet som används för att ta skärmbilder av skärmen. ///  offentligt gränssnitt ITakesScreenshot ///  /// Gets a  objekt som representerar bilden av sidan på skärmen. ///  /// ///  /// A  objekt som innehåller bilden. ///  Skärmdump GetScreenshot (); 

Detta gränssnitt implementeras av webbdrivrutiner och kan användas så här:

var screenshot = driver.GetScreenshot (); screenshot.SaveAsFile ("", ImageFormat.Png);

På det här sättet när ett test misslyckas eftersom du är på en fel sida kan du snabbt räkna ut det genom att kolla på den fångade skärmdumpen.

Även fånga skärmdumpar är inte alltid tillräckligt men. Du kan till exempel se det element du förväntar dig på sidan, men testet misslyckas fortfarande med att det inte hittar det, kanske på grund av fel väljare som leder till misslyckad elementuppslag. Så istället för (eller att komplettera) skärmdumpen kan du också fånga sidkällan som html. Det finns en Sidkälla egendom på IWebDriver gränssnitt (som implementeras av alla webdrivrutiner):

///  /// Hämtar källan till sidan senast laddad av webbläsaren. ///  ///  /// Om sidan har ändrats efter att ha laddats (till exempel med JavaScript) /// finns det ingen garanti för att den returnerade texten är den på den modifierade sidan. /// Vänligen se dokumentationen för den aktuella drivrutinen som används för att /// avgöra om den returnerade texten speglar aktuellt läge på sidan /// eller den text som senast skickades av webbservern. Sidkällan som returneras är en /// representation av den underliggande DOM: förvänta dig inte att den formateras / / eller undviks på samma sätt som svaret från webbservern. ///  sträng PageSource get; 

Precis som vi gjorde med ITakesScreenshot du kan genomföra en metod som tar tag i sidkällan och fortsätter den till en fil för senare inspektion:

File.WriteAllText ("", driver.PageSource);

Du vill inte verkligen fånga skärmdumpar och sidokällor för alla sidor du besöker och för de godkända provningarna. annars måste du gå igenom tusentals av dem när någonting faktiskt går fel. Istället bör du bara fånga dem när ett test misslyckas eller annars när du behöver mer information för felsökning. För att undvika att förorena koden med alltför många provtagningsblock och för att undvika koddubblingar bör du placera alla dina elementuppslag och påståenden i en klass och linda dem med provtagning och sedan fånga skärmdumpen och / eller sidkällan i fångstblocket . Här är lite kod du kan använda för att exekvera åtgärder mot ett element:

public void Execute (By by, Action åtgärd) försök var element = WebDriver.FindElement (by); åtgärden (element);  fånga var capturer = ny Capturer (WebDriver); capturer.CaptureScreenshot (); capturer.CapturePageSource (); kasta; 

De Capturer klassen kan implementeras som:

offentlig klass Capturer offentlig statisk sträng OutputFolder = Path.Combine (AppDomain.CurrentDomain.BaseDirectory, "FailedTests"); privat readonely RemoteWebDriver _webDriver; offentlig Capturer (RemoteWebDriver webDriver) _webDriver = webDriver;  public void CaptureScreenshot (strängfilName = null) var kamera = (ITakesScreenshot) _webDriver; var screenshot = camera.GetScreenshot (); var screenShotPath = GetOutputFilePath (filnamn, "png"); screenshot.SaveAsFile (screenShotPath, ImageFormat.Png);  public void CapturePageSource (strängfilName = null) var filePath = GetOutputFilePath (filnamn, "html"); File.WriteAllText (filePath, _webDriver.PageSource);  privat sträng GetOutputFilePath (strängfilnamn, strängfilExtension) if (! Directory.Exists (OutputFolder)) Directory.CreateDirectory (OutputFolder); var windowTitle = _webDriver.Title; fileName = fileName ?? string.Format ("0 1. 2", windowTitle, DateTime.Now.ToFileTime (), filExtension) .Replace (':', '.'); var outputPath = Path.Combine (OutputFolder, filnamn); var pathChars = Path.GetInvalidPathChars (); var stringBuilder = ny StringBuilder (outputPath); foreach (var-item i pathChars) stringBuilder.Replace (item, '.'); var screenShotPath = stringBuilder.ToString (); returnera screenShotPath; 

Denna implementering fortsätter skärmdumpen och HTML-källan i en mapp som heter FailedTests bredvid testen, men du kan ändra det om du vill ha olika beteenden.

Även om jag bara visade metoder som är specifika för Selen, finns liknande API i alla automationsramar som jag vet och kan enkelt användas.


Slutsats

I den här artikeln pratade vi om några UI-testtips och tricks. Vi diskuterade hur du kan undvika en spröd och långsam UI-testpaket genom att undvika fasta förseningar i dina test. Vi diskuterade sedan hur man undviker spröda väljare och test genom att välja väljare tydligt och även hur man debugger dina gränssnittstester när de misslyckas.

Merparten av koden som visas i den här artikeln finns i MvcMusicStore-provförvaret som vi såg i den senaste artikeln. Det är också värt att notera att mycket kod i MvcMusicStore lånades från Seleno codebase, så om du vill se en massa coola tricks kanske du vill kolla Seleno ut. Ansvarsbegränsning: Jag är medstifter av TestStack organisation och en bidragsgivare på Seleno.

Jag hoppas det vi har diskuterat i den här artikeln hjälper dig i dina granskningsinitiativ.