Nell'ultimo articolo ho parlato di alcune idee e modelli, come il pattern Oggetto pagina, che aiutano a scrivere test UI manutenibili. In questo articolo discuteremo alcuni argomenti avanzati che potrebbero aiutarti a scrivere test più affidabili e a risolverli quando falliscono:
Userò Selenium per gli argomenti di automazione del browser discussi in questo articolo.
Proprio come l'articolo precedente, i concetti e le soluzioni discussi in questo articolo sono applicabili indipendentemente dalla lingua e dalla struttura dell'interfaccia utente che utilizzi. Prima di proseguire, leggere l'articolo precedente, come farò riferimento ad esso e al relativo codice di esempio alcune volte. Non preoccuparti; Aspetterò qui.
Aggiunta Thread.Sleep
(o in genere ritardi) si sente come un inevitabile trucco quando si tratta di test dell'interfaccia utente. Hai un test che fallisce a intermittenza e dopo alcune indagini lo rintracci a ritardi occasionali nella risposta; Ad esempio, si naviga verso una pagina e si guarda o si asserisce qualcosa prima che la pagina sia completamente caricata e il framework di automazione del browser genera un'eccezione che indica che l'elemento non esiste. Un sacco di cose potrebbero contribuire a questo ritardo. Per esempio:
O un mix di questi e altri problemi.
Supponiamo che tu abbia una pagina che normalmente impiega meno di un secondo per caricare, ma i test che lo colpiscono falliscono di tanto in tanto a causa di un ritardo occasionale in risposta. Hai alcune opzioni:
Vedete, non c'è vincita con ritardi arbitrari: si ottiene una suite di test lenta o fragile. Qui ti mostrerò come evitare di inserire ritardi fissi nei tuoi test. Discuteremo due tipi di ritardi che dovrebbero coprire praticamente tutti i casi che devi affrontare: aggiungere un ritardo globale e aspettare che succeda qualcosa.
Se tutte le pagine richiedono circa lo stesso tempo per caricarsi, il che è più lungo del previsto, la maggior parte dei test fallirà a causa di una risposta intempestiva. In casi come questo puoi usare Implicit Attivi:
Un'attesa implicita è di dire a WebDriver di interrogare il DOM per una certa quantità di tempo quando si cerca di trovare un elemento o elementi se non sono immediatamente disponibili. L'impostazione predefinita è 0. Una volta impostata, l'attesa implicita viene impostata per la durata dell'istanza dell'oggetto WebDriver.
Ecco come imposti un'attesa implicita:
Driver WebDriver = nuovo FirefoxDriver (); .. Driver.Manage () Timeout () ImplicitlyWait (TimeSpan.FromSeconds (5));
In questo modo stai dicendo al Selenium di aspettare fino a 5 secondi quando cerca di trovare un elemento o di interagire con la pagina. Quindi ora puoi scrivere:
driver.Url = "http: // somedomain / url_that_delays_loading"; IWebElement myDynamicElement = driver.FindElement (By.Id ("someDynamicElement"));
invece di:
driver.Url = "http: // somedomain / url_that_delays_loading"; Thread.Sleep (5000); IWebElement myDynamicElement = driver.FindElement (By.Id ("someDynamicElement"));
Il vantaggio di questo approccio è questo FindElement
ritornerà non appena trova l'elemento e non attende per tutti i 5 secondi quando l'elemento è disponibile prima.
Una volta che l'attesa implicita è impostata sul tuo WebDriver
istanza si applica a tutte le azioni sul driver; così puoi sbarazzarti di molti Thread.Sleep
s nel tuo codice.
5 secondi sono un'attesa che ho inventato per questo articolo - dovresti trovare l'attesa implicita ottimale per la tua applicazione e dovresti rendere questa attesa il più breve possibile. Dalla documentazione dell'API:
L'aumento del timeout di attesa implicito dovrebbe essere usato con giudizio poiché avrà un effetto negativo sul tempo di esecuzione del test, specialmente se usato con strategie di localizzazione più lente come XPath.
Anche se non si utilizza XPath, l'uso di lunghe attese implicite rallenta i test, in particolare quando alcuni test falliscono davvero, perché il driver Web attende molto tempo prima che scada e genera un'eccezione.
Usare l'attesa implicita è un ottimo modo per sbarazzarsi di molti ritardi hardcoded nel codice; ma ti troverai ancora in una situazione in cui devi aggiungere alcuni ritardi nel codice perché stai aspettando che accada qualcosa: una pagina è più lenta di tutte le altre pagine e devi aspettare più a lungo, sei in attesa che una chiamata AJAX termini o che un elemento appaia o scompaia dalla pagina, ecc. Qui è dove hai bisogno di esplicita attesa.
Quindi hai impostato l'attesa implicita a 5 secondi e funziona per molti dei tuoi test; ma ci sono ancora alcune pagine che a volte richiedono più di 5 secondi per caricarsi e risultare in test non funzionanti.
Come nota a margine, dovresti indagare sul motivo per cui una pagina impiega così tanto tempo prima, prima di provare a correggere il test rotto facendolo attendere più a lungo. Potrebbe esserci un problema di prestazioni nella pagina che sta portando al test rosso, nel qual caso dovresti correggere la pagina, non il test.
In caso di una pagina lenta, è possibile sostituire i ritardi fissi con Explicit Waits:
Un'attesa esplicita è il codice che si definisce per attendere che si verifichi una determinata condizione prima di procedere ulteriormente nel codice.
È possibile applicare l'attesa esplicita usando WebDriverWait
classe. WebDriverWait
vive a WebDriver.Support
assemblaggio e può essere installato utilizzando Selenium.Support nuget:
////// Fornisce la possibilità di attendere una condizione arbitraria durante l'esecuzione del test. /// classe pubblica WebDriverWait: DefaultWait/// /// Inizializza una nuova istanza di /// L'istanza di WebDriver era in attesa.Il valore di timeout che indica per quanto tempo attendere la condizione. pubblico WebDriverWait (driver IWebDriver, timeout TimeSpan); ///classe. /// /// Inizializza una nuova istanza di /// Un oggetto che implementa ilclasse. /// interfaccia utilizzata per determinare quando è trascorso il tempo.L'istanza di WebDriver era in attesa.Il valore di timeout che indica per quanto tempo attendere la condizione.UN valore che indica la frequenza con cui verificare se la condizione è vera. public WebDriverWait (orologio IClock, driver IWebDriver, timeout TimeSpan, TimeSpan sleepInterval);
Ecco un esempio di come puoi usare WebDriverWait
nei tuoi test:
driver.Url = "http: // somedomain / url_that_takes_a_long_time_to_load"; WebDriverWait wait = new WebDriverWait (driver, TimeSpan.FromSeconds (10)); var myDynamicElement = wait.Until (d => d.FindElement (By.Id ("someElement")));
Stiamo dicendo al Selenium che vogliamo aspettare questa particolare pagina / elemento per un massimo di 10 secondi.
È probabile che ci siano alcune pagine che richiedono più tempo dell'attesa implicita predefinita e non è una buona pratica di codifica continuare a ripetere questo codice ovunque. Dopotutto Il codice di prova è codice. Potresti invece estrarlo in un metodo e usarlo dai tuoi test:
public IWebElement FindElementWithWait (By by, int secondsToWait = 10) var wait = new WebDriverWait (WebDriver, TimeSpan.FromSeconds (secondsToWait)); return wait.Until (d => d.FindElement (by));
Quindi puoi usare questo metodo come:
var slowPage = new SlowPage ("http: // somedomain / url_that_takes_a_long_time_to_load"); var element = slowPage.FindElementWithWait (By.Id ("someElement"));
Questo è un esempio forzato per mostrare come potrebbe essere il metodo e come potrebbe essere usato. Idealmente dovresti spostare tutte le interazioni di pagina sugli oggetti della pagina.
Vediamo un altro esempio di un'attesa esplicita. A volte la pagina è completamente caricata ma l'elemento non è ancora lì perché viene successivamente caricato come risultato di una richiesta AJAX. Forse non è un elemento che stai aspettando, ma vuoi solo aspettare che finisca un'interazione AJAX prima di poter fare un'affermazione, ad esempio nel database. Anche in questo caso la maggior parte degli sviluppatori usa Thread.Sleep
per fare in modo che, ad esempio, la chiamata AJAX sia terminata e il record è ora nel database prima di passare alla riga successiva del test. Questo può essere facilmente rettificato utilizzando l'esecuzione di JavaScript!
La maggior parte dei framework di automazione del browser consente di eseguire JavaScript nella sessione attiva e Selenium non fa eccezione. In Selenium c'è un'interfaccia chiamata IJavaScriptExecutor
con due metodi:
////// Definisce l'interfaccia attraverso la quale l'utente può eseguire JavaScript. /// interfaccia pubblica IJavaScriptExecutor ////// Esegue JavaScript nel contesto del frame o della finestra correntemente selezionati. /// /// Il codice JavaScript da eseguire. ////// Il valore restituito dallo script. /// oggetto ExecuteScript (script stringa, oggetto params [] args); ////// Esegue JavaScript in modo asincrono nel contesto del frame o della finestra correntemente selezionati. /// /// Il codice JavaScript da eseguire. ////// Il valore restituito dallo script. /// oggetto ExecuteAsyncScript (script stringa, oggetto params [] args);
Questa interfaccia è implementata da RemoteWebDriver
che è la classe base per tutte le implementazioni dei driver Web. Quindi sulla tua istanza del driver web puoi chiamare ExecuteScript
per eseguire uno script JavaScript. Ecco un metodo che puoi usare per aspettare che tutte le chiamate AJAX finiscano (supponendo che tu stia usando jQuery):
// Si presume che vivi in una classe che ha accesso all'istanza "WebDriver" attiva attraverso il campo / proprietà "WebDriver". pubblico vuoto WaitForAjax (int secondsToWait = 10) var wait = new WebDriverWait (WebDriver, TimeSpan.FromSeconds (secondsToWait)); wait.Until (d => (bool) ((IJavaScriptExecutor) d) .ExecuteScript ("return jQuery.active == 0"));
Combina il ExecuteScript
con WebDriverWait
e tu puoi sbarazzarti di Thread.Sleep
aggiunto per chiamate AJAX.
jQuery.active
restituisce il numero di chiamate AJAX attive avviato da jQuery; quindi quando è zero non ci sono chiamate AJAX in corso. Questo metodo ovviamente funziona solo se tutte le richieste AJAX sono iniziate da jQuery. Se si utilizzano altre librerie JavaScript per le comunicazioni AJAX, è necessario consultare le documentazioni API per un metodo equivalente o tenere traccia delle chiamate AJAX da soli.
Con l'attesa esplicita è possibile impostare una condizione e attendere fino a quando non viene soddisfatta o fino alla scadenza del timeout. Abbiamo visto come potremmo verificare la fine delle chiamate AJAX, un altro esempio è il controllo della visibilità di un elemento. Proprio come il controllo AJAX, potresti scrivere una condizione che controlla la visibilità di un elemento; ma c'è una soluzione più semplice per quello chiamato ExpectedCondition
.
Dalla documentazione Selenium:
Ci sono alcune condizioni comuni che si incontrano di frequente quando si automatizzano i browser web.
Se stai usando Java sei fortunato perché ExpectedCondition
la classe in Java è piuttosto estesa e ha molti metodi di convenienza. Puoi trovare la documentazione qui.
.Gli sviluppatori netti non sono altrettanto fortunati. C'è ancora un ExpectedConditions
classe in WebDriver.Support
assemblaggio (documentato qui) ma è molto minimale:
ExpectedConditions public sealed class ////// Un'aspettativa per controllare il titolo di una pagina. /// /// Il titolo atteso, che deve essere una corrispondenza esatta. ////// statico pubblico Funcquando il titolo corrisponde; altrimenti, . /// TitleIs (titolo stringa); /// /// Un'aspettativa per verificare che il titolo di una pagina contenga una sottostringa sensibile al maiuscolo / minuscolo. /// /// Il frammento di titolo atteso. ////// statico pubblico Funcquando il titolo corrisponde; altrimenti, . /// TitleContains (titolo stringa); /// /// Un'aspettativa per verificare che un elemento sia presente sul DOM di una /// pagina. Questo non significa necessariamente che l'elemento sia visibile. /// /// Il localizzatore cercava l'elemento. ////// Il statico pubblico Funcuna volta individuato. /// ElementExists (Per localizzatore); /// /// Un'aspettativa per verificare che un elemento sia presente sul DOM di una pagina /// e visibile. Visibilità significa che l'elemento non è solo visualizzato ma /// ha anche un'altezza e una larghezza superiore a 0. /// /// Il localizzatore cercava l'elemento. ////// Il statico pubblico Funcuna volta individuato e visibile. /// ElementIsVisible (da locator);
Puoi usare questa classe in combinazione con WebDriverWait
:
var wait = new WebDriverWait (driver, TimeSpan.FromSeconds (3)) var element = wait.Until (ExpectedConditions.ElementExists (By.Id ("foo")));
Come puoi vedere dalla firma della classe sopra puoi controllare il titolo o parti di esso e per l'esistenza e la visibilità degli elementi usando ExpectedCondition
. Il supporto out-of-box in .Net potrebbe essere molto minimale; ma questa classe non è altro che un involucro attorno a semplici condizioni. Puoi facilmente implementare altre condizioni comuni in una classe e usarla con WebDriverWait
dai tuoi script di test.
Un altro gioiello solo per gli sviluppatori Java è FluentWait
. Dalla pagina della documentazione, FluentWait
è
Un'implementazione dell'interfaccia Wait che può avere il timeout e l'intervallo di polling configurati al volo. Ogni istanza di FluentWait definisce la quantità massima di tempo di attesa per una condizione, nonché la frequenza con cui controllare la condizione. Inoltre, l'utente può configurare l'attesa per ignorare determinati tipi di eccezioni durante l'attesa, come NoSuchElementExceptions durante la ricerca di un elemento nella pagina.
Nel seguente esempio stiamo cercando di trovare un elemento con id foo
nella pagina che esegue il polling ogni cinque secondi per un massimo di 30 secondi:
// Attendo 30 secondi affinché un elemento sia presente nella pagina, controllando // per la sua presenza una volta ogni cinque secondi. Aspettarewait = new FluentWait (driver) .withTimeout (30, SECONDS) .pollingEvery (5, SECONDS) .ignoring (NoSuchElementException.class); WebElement foo = wait.until (nuova funzione () public WebElement apply (driver WebDriver) return driver.findElement (By.id ("foo")); );
Ci sono due cose in sospeso FluentWait
: in primo luogo consente di specificare l'intervallo di polling che potrebbe migliorare le prestazioni del test e in secondo luogo consente di ignorare le eccezioni a cui non si è interessati.
FluentWait
è davvero fantastico e sarebbe bello se esistesse anche un equivalente in .Net. Detto questo non è così difficile da implementare usando WebDriverWait
.
Hai i tuoi oggetti Page in posizione, hai un bel codice di test mantenibile DRY e stai anche evitando ritardi fissi nei test; ma i tuoi test falliscono ancora!
L'interfaccia utente è in genere la parte più frequentemente modificata di un'applicazione tipica: a volte si spostano elementi in una pagina per modificare il design della pagina e talvolta le modifiche della struttura della pagina in base ai requisiti. Queste modifiche al layout e al design della pagina potrebbero portare a un sacco di test non funzionanti se non si scelgono i selettori con saggezza.
Non utilizzare selettori sfocati e non fare affidamento sulla struttura della pagina.
Molte volte mi è stato chiesto se va bene aggiungere un ID agli elementi nella pagina solo per il test, e la risposta è un sonoro sì. Per rendere testabile la nostra unità di codice, apportiamo molte modifiche, ad esempio aggiungere interfacce e utilizzare Dependency Injection. Il codice di prova è codice. Fai quello che serve per sostenere i tuoi test.
Diciamo che abbiamo una pagina con il seguente elenco:
In uno dei miei test voglio fare clic sull'album "Let There Be Rock". Vorrei chiedere guai se ho usato il seguente selettore:
By.XPath ( "// ul [@ id = 'album-list'] / li [3] / a")
Quando possibile, devi aggiungere l'ID agli elementi e indirizzarli direttamente e senza fare affidamento sui loro elementi circostanti. Quindi ho intenzione di fare una piccola modifica alla lista:
ho aggiunto id
attribuisce agli ancore in base all'ID degli album univoci in modo da poter indirizzare direttamente un collegamento senza doverlo passare ul
e Li
elementi. Così ora posso sostituire il selezionatore fragile con By.Id ( "album-35")
che è garantito per funzionare fintanto che l'album è sulla pagina, che a proposito è una buona asserzione. Per creare quel selettore dovrei ovviamente avere accesso all'ID dell'album dal codice di test.
Tuttavia, non è sempre possibile aggiungere id univoci agli elementi, come le righe di una griglia o gli elementi di un elenco. In casi come questo è possibile utilizzare le classi CSS e gli attributi di dati HTML per allegare proprietà rintracciabili ai propri elementi per una più facile selezione. Ad esempio, se nella tua pagina hai due elenchi di album, uno come risultato della ricerca dell'utente e un altro per gli album suggeriti in base agli acquisti precedenti dell'utente, puoi differenziarli utilizzando una classe CSS sul ul
elemento, anche se quella classe non è utilizzata per lo stile della lista:
Se preferisci non avere classi CSS inutilizzate, potresti invece utilizzare gli attributi di dati HTML e modificare gli elenchi in:
e:
Uno dei motivi principali per cui i test dell'interfaccia utente hanno esito negativo è che un elemento o un testo non viene trovato nella pagina. A volte questo accade perché ti atterri su una pagina sbagliata a causa di errori di navigazione, o modifiche alla navigazione delle pagine nel tuo sito web, o errori di validazione. Altre volte potrebbe essere a causa di una pagina mancante o di un errore del server.
Indipendentemente da quali sono le cause dell'errore e se questo viene visualizzato nel registro del server CI o nella console di test del desktop, a NoSuchElementException
(o simili) non è abbastanza utile per capire cosa è andato storto, vero? Quindi, quando il test fallisce, l'unico modo per risolvere l'errore è eseguirlo di nuovo e guardarlo come fallisce. Ci sono alcuni accorgimenti che potrebbero potenzialmente evitare di rieseguire i tuoi test dell'interfaccia utente lenti per la risoluzione dei problemi. Una soluzione è quella di catturare uno screenshot ogni volta che un test fallisce, in modo che possiamo rinviarlo più tardi.
C'è un'interfaccia in Selenium chiamata ITakesScreenshot
:
////// Definisce l'interfaccia utilizzata per acquisire le immagini dello schermo sullo schermo. /// interfaccia pubblica ITakesScreenshot ////// Ottiene a /// ///oggetto che rappresenta l'immagine della pagina sullo schermo. /// /// A Screenshot GetScreenshot ();oggetto contenente l'immagine. ///
Questa interfaccia è implementata da classi di driver Web e può essere utilizzata in questo modo:
var screenshot = driver.GetScreenshot (); screenshot.SaveAsFile ("", ImageFormat.Png);
In questo modo, quando un test fallisce perché ti trovi su una pagina sbagliata, puoi rapidamente individuarlo controllando lo screenshot catturato.
Anche catturare schermate non è sempre sufficiente. Ad esempio, potresti vedere l'elemento che ti aspetti sulla pagina ma il test fallisce ancora dicendo che non lo trova, forse a causa del selettore sbagliato che porta alla ricerca di elementi non riusciti. Quindi, invece di (o per completare) lo screenshot, potresti anche catturare il sorgente della pagina come html. C'è un PageSource
proprietà su IWebDriver
interfaccia (che è implementata da tutti i driver web):
////// Ottiene l'origine della pagina caricata per ultimo dal browser. /// ////// Se la pagina è stata modificata dopo il caricamento (ad esempio, in JavaScript) /// non è possibile garantire che il testo restituito sia quello della pagina modificata. /// Si prega di consultare la documentazione del driver specifico utilizzato per /// determinare se il testo restituito riflette lo stato corrente della pagina /// o l'ultimo testo inviato dal server web. L'origine della pagina restituita è una /// rappresentazione del DOM sottostante: non aspettarti che sia formattato /// o scappato nello stesso modo della risposta inviata dal server web. /// stringa PageSource get;
Proprio come abbiamo fatto con ITakesScreenshot
è possibile implementare un metodo che acquisisce l'origine della pagina e lo mantiene in un file per un'ispezione successiva:
File.WriteAllText ("", driver.PageSource);
Non vuoi veramente catturare schermate e fonti di pagine di tutte le pagine che visiti e per i test di passaggio; altrimenti dovrai attraversarne migliaia quando qualcosa va davvero storto. Invece dovresti catturarli solo quando un test fallisce o quando hai bisogno di più informazioni per la risoluzione dei problemi. Per evitare di inquinare il codice con troppi blocchi try-catch e per evitare duplicazioni di codice dovresti mettere tutte le ricerche e le asserzioni di elementi in una classe e avvolgerle con try-catch e quindi catturare lo screenshot e / o l'origine della pagina nel blocco catch . Ecco un po 'di codice che potresti usare per eseguire azioni su un elemento:
public public Execute (By by, Azioneaction) try var element = WebDriver.FindElement (by); azione (elemento); catch var capturer = new Capturer (WebDriver); capturer.CaptureScreenshot (); capturer.CapturePageSource (); gettare;
Il Capturer
la classe può essere implementata come:
public class Capturer stringa statica pubblica OutputFolder = Path.Combine (AppDomain.CurrentDomain.BaseDirectory, "FailedTests"); privato readonly RemoteWebDriver _webDriver; public Capturer (RemoteWebDriver webDriver) _webDriver = webDriver; public void CaptureScreenshot (string fileName = null) var camera = (ITakesScreenshot) _webDriver; var screenshot = camera.GetScreenshot (); var screenShotPath = GetOutputFilePath (fileName, "png"); screenshot.SaveAsFile (screenShotPath, ImageFormat.Png); public void CapturePageSource (string fileName = null) var filePath = GetOutputFilePath (fileName, "html"); File.WriteAllText (filePath, _webDriver.PageSource); stringa privata GetOutputFilePath (string fileName, string fileExtension) if (! Directory.Exists (OutputFolder)) Directory.CreateDirectory (OutputFolder); var windowTitle = _webDriver.Title; fileName = fileName ?? string.Format ("0 1. 2", windowTitle, DateTime.Now.ToFileTime (), fileExtension) .Replace (':', '.'); var outputPath = Path.Combine (OutputFolder, fileName); var pathChars = Path.GetInvalidPathChars (); var stringBuilder = new StringBuilder (outputPath); foreach (var item in pathChars) stringBuilder.Replace (item, '.'); var screenShotPath = stringBuilder.ToString (); return screenShotPath;
Questa implementazione mantiene lo screenshot e l'origine HTML in una cartella denominata FailedTests accanto ai test, ma è possibile modificarla se si desidera un comportamento diverso.
Sebbene mostri solo metodi specifici per il selenio, esistono API simili in tutti i framework di automazione che conosco e che possono essere facilmente utilizzati.
In questo articolo abbiamo parlato di alcuni suggerimenti e trucchi per l'interfaccia utente. Abbiamo discusso su come evitare una suite di test dell'interfaccia utente fragile e lenta, evitando ritardi fissi nei test. Abbiamo poi discusso su come evitare selettori e test fragili scegliendo saggiamente i selettori e anche come eseguire il debug dei test dell'interfaccia utente quando falliscono.
La maggior parte del codice mostrato in questo articolo può essere trovato nel repository di esempio MvcMusicStore che abbiamo visto nell'ultimo articolo. Vale anche la pena notare che un sacco di codice nel MvcMusicStore è stato preso in prestito dalla base di codice Seleno, quindi se vuoi vedere un sacco di trucchi interessanti potresti voler controllare Seleno. Disclaimer: Sono un co-fondatore dell'organizzazione TestStack e collaboratore di Seleno.
Spero che ciò di cui abbiamo discusso in questo articolo ti aiuti nelle prove del tuo UI.