À propos de la pertinence de Selenium WebDriverWait

Plus je me rapproche de Selenium WebDriver, plus je me demande pourquoi telle ou telle fonctionnalité est implémentée de cette façon et pas autrement. Dans son discours, «Troubles in Selenium WebDriver», Alexey Barantsev met en lumière les subtilités de la mise en œuvre de cet outil d'automatisation et fait la distinction entre «bugs» et «fonctionnalités». Vous trouverez beaucoup de choses intéressantes dans la vidéo, mais certains points restent (au moins pour moi) dans l'ombre.

Dans cet article, je veux discuter de l'outil fréquemment utilisé pour attendre un événement sur une page, implémenté à l'aide de la classe WebDriverWait et de sa principale méthode Until . Je me demande si WebDriverWait est nécessaire et est-il possible de le refuser?

Les réflexions seront présentées dans le contexte de C #, bien que je ne pense pas que la logique d'implémentation de cette classe sera différente pour les autres langages de programmation.

Lors de la création d'une instance WebDriverWait, une instance de pilote est transmise au constructeur, qui est stockée dans le champ de saisie d'entrée . La méthode Until suppose un délégué dont le paramètre d'entrée doit être IWebDriver, dont l'entrée est une instance.

Regardons le code source de la méthode Until . L'épine dorsale de sa logique est un cycle sans fin avec deux conditions pour en sortir: le début de l'événement ou du timeout souhaité. Des «goodies» supplémentaires ignorent les exceptions prédéfinies et retournent l'objet si TROOSULT n'est pas booléen (plus à ce sujet plus tard).

La première limitation que je vois est que nous avons toujours besoin exactement d'une instance de IWebDriver, bien qu'à l'intérieur de la méthode Until (pour être précis, en tant que paramètre d'entrée pour la condition), nous puissions gérer complètement ISearchContext. En effet, dans la plupart des cas, nous attendons un élément ou un changement dans sa propriété et utilisons FindElement (s) pour le rechercher.

Je risque de déclarer que l'utilisation de ISearchContext serait encore plus logique, car le code client (classe) n'est pas seulement un objet de page, qui, dans la recherche d'enfants, est repoussé depuis la racine de la page. Parfois, il s'agit d'une classe qui décrit un élément composite dont la racine est un autre élément de la page, et non la page elle-même. Un exemple de ceci est SelectElement , qui accepte une référence au parent IWebElement dans le constructeur.

Revenons au problème de l'initialisation de WebDriverWait. Cette action nécessite une instance du pilote. C'est-à-dire nous avons toujours, d'une manière ou d'une autre, besoin de lancer une instance de IWebDriver de l'extérieur du code client, même s'il s'agit d'une classe d'un élément composite (un exemple sur SelectElement), qui accepte déjà un "parent". De mon point de vue, ce n'est pas nécessaire.

Bien sûr, nous pouvons déclarer une classe par analogie
SearchContextWait : DefaultWait<ISearchContext> 
Mais ne nous précipitons pas. Nous n'avons pas besoin de lui.

Voyons comment l'instance de pilote passée à condition est utilisée. Habituellement, cela ressemble à ceci:

 var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); wait.Until( d => d.FindElements(By.XPath("locator")).Count > 0 ); 

La question se pose, pourquoi une version «locale» du pilote est-elle nécessaire si la condition est toujours disponible à partir du code client? De plus, il s'agit de la même instance passée précédemment via le constructeur. C'est-à-dire le code pourrait ressembler à ceci:

 var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); wait.Until( d => Driver.FindElements(By.XPath("locator")).Count > 0 ); 

Même Simon Stewart utilise cette approche dans son discours .

image

Il n'écrit pas «d -> d.», Mais écrit «d -> driver.», C'est-à-dire l'instance de pilote passée dans la méthode est simplement ignorée. Mais il faut le transmettre, car cela est requis par la signature de la méthode!

Pourquoi faire passer le pilote dans l'état de la méthode? Il est possible d'isoler la recherche à l'intérieur de cette méthode, telle qu'implémentée dans ExpectedConditions ? Jetez un œil à l'implémentation de la méthode TextToBePresentInElement . Ou VisibilityOfAllElementsLocatedBy . Ou TextToBePresentInElementValue . Le pilote transféré n'y est même pas utilisé!

Ainsi, la première pensée est que nous n'avons pas besoin de la méthode Until avec le paramètre délégué que le pilote accepte.

Voyons maintenant si la méthode Until a besoin d'une valeur de retour? Si bool agit comme TResult, alors non, ce n'est pas nécessaire. En effet, en cas de succès, vous obtiendrez vrai, et en cas d'échec, vous obtiendrez une TimeoutException. Quel est le contenu informationnel d'un tel comportement?

Mais que faire si l'objet est TResult? Supposons la construction suivante:

 var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); wait.IgnoreExceptionTypes(typeof(NoSuchElementException)); var element = wait.Until(d => d.FindElement(By.XPath("locator"))); 

C'est-à-dire nous attendons non seulement l'apparition de l'élément, mais nous l'utilisons également (si nous avons attendu), supprimant ainsi un appel inutile au DOM. Bon.

Examinons de plus près ces trois lignes de code. Dans l'implémentation de la méthode Until, cela se résume à une sorte de similitude (code conditionnel)

 try { FindElement } catch (NoSuchElementException) {} 

C'est-à-dire une exception sera levée à chaque fois jusqu'à ce que l'élément apparaisse dans le DOM. La génération d'exception étant un événement assez cher, je préférerais l'éviter, surtout dans les endroits où ce n'est pas difficile. Nous pouvons réécrire le code comme suit:

 var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); var elements = wait.Until(d => d.FindElements(By.XPath("locator"))); 

C'est-à-dire nous utilisons FindElements, qui ne lève pas d'exception. Attendez, ce design attendra-t-il l'apparition des éléments? NON! Parce que, si vous regardez le code source , une boucle infinie se termine immédiatement, dès que la condition retourne non nulle. Et FindElements en cas d'échec renvoie une collection vide, mais nullement null. C'est-à-dire Pour une liste d'éléments, l'utilisation de Jusqu'à n'a aucun sens.

Ok, la liste est claire. Mais quand même, comment retourner l'élément trouvé et ne pas lever d'exception? Le code pourrait ressembler à ceci:

 var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); var element = wait.Until(d => d.FindElements(By.XPath("locator")).FirstOrDefault()); 

Dans ce cas, à chaque itération de la boucle, nous n'obtiendrons pas seulement la liste IWebElement (qui peut être vide), mais essaierons également d'en extraire le premier élément. Si les éléments ne sont toujours pas affichés sur la page, nous obtiendrons null (valeur par défaut pour l'objet) et passerons à la prochaine itération de la boucle. Si l'élément est trouvé, nous quitterons la méthode et la variable d'élément sera initialisée avec la valeur de retour.

Et pourtant, la deuxième pensée - la valeur de retour de la méthode Until n'est pas utilisée dans la plupart des cas.

La valeur transmise n'est pas nécessaire, la valeur de retour n'est pas utilisée. Quelle est l'utilité de Until? Uniquement dans le cycle et la fréquence d'appel de la méthode de la condition? Cette approche a déjà été implémentée en C # dans la méthode SpinWait.SpinUntil . Sa seule différence est qu'il ne lève pas d'exception de délai d'expiration. Cela peut être résolu comme suit:

 public void Wait(Func<bool> condition, TimeSpan timeout) { var waited = SpinWait.SpinUntil(condition, timeout); if (!waited) { throw new TimeoutException(); } } 

C'est-à-dire ces quelques lignes de code remplacent dans la plupart des cas la logique de l'ensemble de la classe WebDriverWait. Les efforts valent-ils le résultat?

Mettre à jour

Dans les commentaires de l'article, l'utilisateur de KSA a fait une remarque raisonnable sur la différence entre SpinUntil et Until en termes de fréquence de la condition. Pour WebDriverWait, cette valeur est réglable et par défaut à 500 millisecondes. C'est-à-dire la méthode Until a un délai entre les itérations de boucle. Alors que pour SpinUntil la logique est légèrement compliquée et souvent l'attente ne dépasse pas 1 milliseconde.

En pratique, cela se traduit par une situation où, en attendant un élément qui apparaît dans les 2 secondes, la méthode Unitl effectue 4 itérations, et la méthode SpinUntil prend environ 200 ou plus.

Jetons SpinUntil et réécrivons la méthode Wait comme suit.

 public void Wait(Func<bool> condition, TimeSpan timeout, int evaluatedInterval = 500) { Stopwatch sw = Stopwatch.StartNew(); while (sw.Elapsed < timeout) { if (condition()) { return; } Thread.Sleep(evaluatedInterval); } throw new TimeoutException(); } 


Nous avons ajouté quelques lignes de code, et en même temps nous nous sommes rapprochés de la logique de la méthode Until.

Source: https://habr.com/ru/post/fr443754/


All Articles