自動化されたWebアプリケーションテスト(MS単体テストフレームワーク+ Selenium WebDriver C#)。 パート2.2:Selenium APIラッパー-WebElement

セレン+ C#

はじめに


こんにちは 前のパートでは、Selenium WebDriverで作業するときに生じる主な問題について説明し、ブラウザーラッパーの例を示しました。 難しくはありませんでしたね?)では、先に進みましょう。 残りの問題に対処する必要があります。



このパートでは、完全にユーザーを対象としたラッパーWebElementを作成します。 自動テストの開発者に。 執筆時点で、私のテストは、手動テストエンジニアが自動テストを作成するために使用する「フレームワーク」を作成することでしたと認めています。 当然、彼らは非常に控えめなプログラミング知識を持っているはずでした。 したがって、フレームワーク自体に含まれるコードの数や、内部にあるコードの複雑さはまったく関係ありませんでした。 主なものは、それの外側は3文字のように単純であることです。 私はあなたに警告します、多くのコードといくつかの写真があります=)



参照資料


パート1:はじめに

パート2.1:Selenium APIラッパー-ブラウザー

パート2.2:Selenium APIラッパー-WebElement

パート3:WebPages-ページの説明

パート4:最後にテストを書く

フレームワークの公開



どうぞ!


それで、自動テストの開発者として、Web要素を説明することがどのように便利かを考え始めました。 一部の開発者がゲッターを記述することで解決する最初の問題は、次のようになります。

private IWebElement LoginEdit { get { return WebDriver.FindElement(By.Id("Login")); } }
      
      





一意のプロパティがない場合は、FindElementsを使用して一連のプロパティで検索し、GetAttributeおよびGetCssValueを使用してそれらをフィルタリングする必要があります。



WebDriver.Supportには、PageFactoryやFindsBy属性などの機能があります。

 [FindsBy(How = How.LinkText, Using = "")] public IWebElement HelpLink { get; set; }
      
      





プロパティの説明は属性を介して行われます-悪くありません。 さらに、検索をキャッシュすることもできます(CacheLookup)。 この決定の短所:



原則として、多くの人がこれで終わります。 さらに進んでいきます。 私は途中で取得したいいくつかの利益を策定します。



アイデアとユースケース


主なアイデアは、要素を記述するときに検索条件を入力し、それを使用したアクション中に要素検索自体を実行することです。 さらに、最適なテストパフォーマンスのために検索結果のキャッシュを実装したいと思います。



要素を1行で記述することも非常に便利ですが、プロパティの配列を作成して渡すことはできません。 そして、ここで、ところで、「呼び出しチェーン」パターンがあります。 また、パラメータの出現により要素を検索できる必要があります。



さて、完全に幸福にするためには、たとえば、Linqスタイルの要素にグループメソッドを実装して、何らかの基準に従ってすべてのチェックボックスを配置するか、リンクの配列から文字列の配列を取得する必要があります。



WebElementスキームを表示してみます。

画像



同じように、複雑なアプリケーションをテストする場合、Selenium WebDriverを使用して要素を認識できない場合があります。 この問題を解決するために、Browser.ExecuteJavaScriptメソッドが提供されています(前の記事を参照)。 JavaScriptとjQueryを使用して要素を操作することができます。



ラッパーコードに移る前に、説明の例を示します。



IDで検索:

 private static readonly WebElement TestElement = new WebElement().ById("StartButton");
      
      





XPath検索:

 private static readonly WebElement TestElement = new WebElement().ByXPath("//div[@class='Content']//tr[2]/td[2]");
      
      





クラスごとに最後のアイテムを検索します。

 private static readonly WebElement TestElement = new WebElement().ByClass("UserAvatar").Last();
      
      





属性の値で検索:

 private static readonly WebElement TestElement = new WebElement().ByAttribute(TagAttributes.Href, "TagEdit", exactMatch: false);
      
      





いくつかのパラメーターで検索:

 private static readonly WebElement TestElement = new WebElement().ByClass("TimePart").ByName("Day").Index(0);
      
      





タグとテキストで検索(エントリ):

 private static readonly WebElement TestElement = new WebElement().ByTagName(TagNames.Link).ByText("Hello", exactMach);
      
      





TestElementは、必ずしも単一の要素の説明を格納するわけではないことに注意してください。 複数の要素がある場合、クリックしようとすると例外が発生するはずです(ただし、私の実装では最初に遭遇した要素が使用されます)。 インデックス(...)を使用して要素のインデックスを指定することもできます。最初()または最後()のいずれかで、1つの要素が見つかることが保証されます。 さらに、1つの要素でアクションを実行する必要はありません。一度にすべての要素でアクションを実行できます(以下の例のForEachを参照)。



次に、使用例を示します。



アイテムをクリック

 TestElement.Click();
      
      





Selenium WebDriverまたはjQueryを使用して要素をクリックします。

 TestElement.Click(useJQuery: true);
      
      





テキストの取得(例:リンクまたはテキストフィールド):

 var text = TestElement.Text;
      
      





テキスト設定:

 TestElement.Text = "Hello!";
      
      





アイテムを別のアイテムにドラッグ:

 TestElement1.DragAndDrop(TestElement2);
      
      





イベントを要素に送信する:

 TestElement.FireJQueryEvent(JavaScriptEvents.KeyUp);
      
      





折りたたまれた要素をすべて展開します(プラス記号をクリックします)。

 TestElements.ForEach(i => i.Click());
      
      





すべてのヘッダーの値を取得する:

 var subjects = new WebElement().ByClass("Subject").Select(i => i.Text);
      
      







適用されたコールチェーンパターンにより、要素を決定すると同時にアクションを実行できます。

 new WebElement().ById("Next").Click(); var text = new WebElement().ById("Help").Text;
      
      





エンドユーザー(ページの要素を説明する自動テストの開発者)にとって、とてもフレンドリーに見えますよね? 何も出てこない。 開発者が、このために列挙型TagAttributesとTagNamesを使用して、任意の属性とタグ名をパラメーターとして渡すことさえできないことに注意してください。 これにより、多数の魔法の文字列からコードが保存されます。



残念ながら、そのようなAPIを提供するには、多くのコードを作成する必要があります。 WebElement(部分)クラスは5つの部分に分割されます:



前の記事で既に警告したように、コードにはコメントがありませんが、コピーアンドペーストの主要な点についてコメントしようとします。



WebElement.cs


 namespace Autotests.Utilities.WebElement { public partial class WebElement : ICloneable { private By _firstSelector; private IList<IWebElement> _searchCache; private IWebElement FindSingle() { return TryFindSingle(); } private IWebElement TryFindSingle() { Contract.Ensures(Contract.Result<IWebElement>() != null); try { return FindSingleIWebElement(); } catch (StaleElementReferenceException) { ClearSearchResultCache(); return FindSingleIWebElement(); } catch (InvalidSelectorException) { throw; } catch (WebDriverException) { throw; } catch (WebElementNotFoundException) { throw; } catch { throw WebElementNotFoundException; } } private IWebElement FindSingleIWebElement() { var elements = FindIWebElements(); if (!elements.Any()) throw WebElementNotFoundException; var element = elements.Count() == 1 ? elements.Single() : _index == -1 ? elements.Last() : elements.ElementAt(_index); // ReSharper disable UnusedVariable var elementAccess = element.Enabled; // ReSharper restore UnusedVariable return element; } private IList<IWebElement> FindIWebElements() { if (_searchCache != null) { return _searchCache; } Browser.WaitReadyState(); Browser.WaitAjax(); var resultEnumerable = Browser.FindElements(_firstSelector); try { resultEnumerable = FilterByVisibility(resultEnumerable).ToList(); resultEnumerable = FilterByTagNames(resultEnumerable).ToList(); resultEnumerable = FilterByText(resultEnumerable).ToList(); resultEnumerable = FilterByTagAttributes(resultEnumerable).ToList(); resultEnumerable = resultEnumerable.ToList(); } catch (Exception e) { Console.WriteLine(e); return new List<IWebElement>(); } var resultList = resultEnumerable.ToList(); return resultList; } private WebElementNotFoundException WebElementNotFoundException { get { CheckConnectionFailure(); return new WebElementNotFoundException(string.Format("Can't find single element with given search criteria: {0}.", SearchCriteriaToString())); } } private static void CheckConnectionFailure() { const string connectionFailure = "connectionFailure"; Contract.Assert(!Browser.PageSource.Contains(connectionFailure), "Connection can't be established."); } object ICloneable.Clone() { return Clone(); } public WebElement Clone() { return (WebElement)MemberwiseClone(); } } }
      
      





ここでは、FindIWebElements、FindSingleIWebElement、およびTryFindSingleの例外処理に主な注意が払われています。 FindIWebElementsでは、ブラウザーがすべての作業(WaitReadyStateおよびWaitAjax)を完了するのを待って、要素(FindElements)を検索し、さまざまな基準に従ってそれらをフィルターします。 また、コードに_searchCacheが表示されます。これは単なるキャッシュです(検索は自動的にキャッシュされないため、要素でCacheSearchResultメソッドを呼び出す必要があります)。



WebElementActions.cs


 namespace Autotests.Utilities.WebElement { internal enum SelectTypes { ByValue, ByText } public partial class WebElement { #region Common properties public int Count { get { return FindIWebElements().Count; } } public bool Enabled { get { return FindSingle().Enabled; } } public bool Displayed { get { return FindSingle().Displayed; } } public bool Selected { get { return FindSingle().Selected; } } public string Text { set { var element = FindSingle(); if (element.TagName == EnumHelper.GetEnumDescription(TagNames.Input) || element.TagName == EnumHelper.GetEnumDescription(TagNames.TextArea)) { element.Clear(); } else { element.SendKeys(Keys.LeftControl + "a"); element.SendKeys(Keys.Delete); } if (string.IsNullOrEmpty(value)) return; Browser.ExecuteJavaScript(string.Format("arguments[0].value = \"{0}\";", value), element); Executor.Try(() => FireJQueryEvent(JavaScriptEvents.KeyUp)); } get { var element = FindSingle(); return !string.IsNullOrEmpty(element.Text) ? element.Text : element.GetAttribute(EnumHelper.GetEnumDescription(TagAttributes.Value)); } } public int TextInt { set { Text = value.ToString(CultureInfo.InvariantCulture); } get { return Text.ToInt(); } } public string InnerHtml { get { return Browser.ExecuteJavaScript("return arguments[0].innerHTML;", FindSingle()).ToString(); } } #endregion #region Common methods public bool Exists() { return FindIWebElements().Any(); } public bool Exists(TimeSpan timeSpan) { return Executor.SpinWait(Exists, timeSpan, TimeSpan.FromMilliseconds(200)); } public bool Exists(int seconds) { return Executor.SpinWait(Exists, TimeSpan.FromSeconds(seconds), TimeSpan.FromMilliseconds(200)); } public void Click(bool useJQuery = true) { var element = FindSingle(); Contract.Assert(element.Enabled); if (useJQuery && element.TagName != EnumHelper.GetEnumDescription(TagNames.Link)) { FireJQueryEvent(element, JavaScriptEvents.Click); } else { try { element.Click(); } catch (InvalidOperationException e) { if (e.Message.Contains("Element is not clickable")) { Thread.Sleep(2000); element.Click(); } } } } public void SendKeys(string keys) { FindSingle().SendKeys(keys); } public void SetCheck(bool value, bool useJQuery = true) { var element = FindSingle(); Contract.Assert(element.Enabled); const int tryCount = 10; for (var i = 0; i < tryCount; i++) { element = FindSingle(); Set(value, useJQuery); if (element.Selected == value) { return; } } Contract.Assert(element.Selected == value); } public void Select(string optionValue) { SelectCommon(optionValue, SelectTypes.ByValue); } public void Select(int optionValue) { SelectCommon(optionValue.ToString(CultureInfo.InvariantCulture), SelectTypes.ByValue); } public void SelectByText(string optionText) { SelectCommon(optionText, SelectTypes.ByText); } public string GetAttribute(TagAttributes tagAttribute) { return FindSingle().GetAttribute(EnumHelper.GetEnumDescription(tagAttribute)); } #endregion #region Additional methods public void SwitchContext() { var element = FindSingle(); Browser.SwitchToFrame(element); } public void CacheSearchResult() { _searchCache = FindIWebElements(); } public void ClearSearchResultCache() { _searchCache = null; } public void DragAndDrop(WebElement destination) { var source = FindSingle(); var dest = destination.FindSingle(); Browser.DragAndDrop(source, dest); } public void FireJQueryEvent(JavaScriptEvents javaScriptEvent) { var element = FindSingle(); FireJQueryEvent(element, javaScriptEvent); } public void ForEach(Action<WebElement> action) { Contract.Requires(action != null); CacheSearchResult(); Enumerable.Range(0, Count).ToList().ForEach(i => action(ByIndex(i))); ClearSearchResultCache(); } public List<T> Select<T>(Func<WebElement, T> action) { Contract.Requires(action != null); var result = new List<T>(); ForEach(e => result.Add(action(e))); return result; } public List<WebElement> Where(Func<WebElement, bool> action) { Contract.Requires(action != null); var result = new List<WebElement>(); ForEach(e => { if (action(e)) result.Add(e); }); return result; } public WebElement Single(Func<WebElement, bool> action) { return Where(action).Single(); } #endregion #region Helpers private void Set(bool value, bool useJQuery = true) { if (Selected ^ value) { Click(useJQuery); } } private void SelectCommon(string option, SelectTypes selectType) { Contract.Requires(!string.IsNullOrEmpty(option)); var element = FindSingle(); Contract.Assert(element.Enabled); switch (selectType) { case SelectTypes.ByValue: new SelectElement(element).SelectByValue(option); return; case SelectTypes.ByText: new SelectElement(element).SelectByText(option); return; default: throw new Exception(string.Format("Unknown select type: {0}.", selectType)); } } private void FireJQueryEvent(IWebElement element, JavaScriptEvents javaScriptEvent) { var eventName = EnumHelper.GetEnumDescription(javaScriptEvent); Browser.ExecuteJavaScript(string.Format("$(arguments[0]).{0}();", eventName), element); } #endregion } public enum JavaScriptEvents { [Description("keyup")] KeyUp, [Description("click")] Click } }
      
      





アイテムに定義されたプロパティとメソッドのフラットリスト。 useJQueryパラメーターを使用するものもあります。このパラメーターは、アクションにJQueryを使用する価値があることをメソッドに伝えます(複雑なケースおよび3つすべてのブラウザーでアクションを実行するために行われます)。 さらに、JavaScriptの実行ははるかに高速です。 一部のメソッドには松葉杖があります。たとえば、SetCheckのtryCountを使用したループです。 もちろん、テストされる各製品には特別なケースがあります。



WebElementByCriteria.cs


 namespace Autotests.Utilities.WebElement { internal class SearchProperty { public string AttributeName { get; set; } public string AttributeValue { get; set; } public bool ExactMatch { get; set; } } internal class TextSearchData { public string Text { get; set; } public bool ExactMatch { get; set; } } public partial class WebElement { private readonly IList<SearchProperty> _searchProperties = new List<SearchProperty>(); private readonly IList<TagNames> _searchTags = new List<TagNames>(); private bool _searchHidden; private int _index; private string _xPath; private TextSearchData _textSearchData; public WebElement ByAttribute(TagAttributes tagAttribute, string attributeValue, bool exactMatch = true) { return ByAttribute(EnumHelper.GetEnumDescription(tagAttribute), attributeValue, exactMatch); } public WebElement ByAttribute(TagAttributes tagAttribute, int attributeValue, bool exactMatch = true) { return ByAttribute(EnumHelper.GetEnumDescription(tagAttribute), attributeValue.ToString(), exactMatch); } public WebElement ById(string id, bool exactMatch = true) { return ByAttribute(TagAttributes.Id, id, exactMatch); } public WebElement ById(int id, bool exactMatch = true) { return ByAttribute(TagAttributes.Id, id.ToString(), exactMatch); } public WebElement ByName(string name, bool exactMatch = true) { return ByAttribute(TagAttributes.Name, name, exactMatch); } public WebElement ByClass(string className, bool exactMatch = true) { return ByAttribute(TagAttributes.Class, className, exactMatch); } public WebElement ByTagName(TagNames tagName) { var selector = By.TagName(EnumHelper.GetEnumDescription(tagName)); _firstSelector = _firstSelector ?? selector; _searchTags.Add(tagName); return this; } public WebElement ByXPath(string xPath) { Contract.Assume(_firstSelector == null, "XPath can be only the first search criteria."); _firstSelector = By.XPath(xPath); _xPath = xPath; return this; } public WebElement ByIndex(int index) { _index = index; return this; } public WebElement First() { _index = 0; return this; } public WebElement Last() { _index = -1; return this; } public WebElement IncludeHidden() { _searchHidden = true; return this; } public WebElement ByText(string text, bool exactMatch = true) { var selector = exactMatch ? By.XPath(string.Format("//*[text()=\"{0}\"]", text)) : By.XPath(string.Format("//*[contains(text(), \"{0}\")]", text)); _firstSelector = _firstSelector ?? selector; _textSearchData = new TextSearchData { Text = text, ExactMatch = exactMatch }; return this; } private WebElement ByAttribute(string tagAttribute, string attributeValue, bool exactMatch = true) { var xPath = exactMatch ? string.Format("//*[@{0}=\"{1}\"]", tagAttribute, attributeValue) : string.Format("//*[contains(@{0}, \"{1}\")]", tagAttribute, attributeValue); var selector = By.XPath(xPath); _firstSelector = _firstSelector ?? selector; _searchProperties.Add(new SearchProperty { AttributeName = tagAttribute, AttributeValue = attributeValue, ExactMatch = exactMatch }); return this; } private string SearchCriteriaToString() { var result = _searchProperties.Select(searchProperty => string.Format("{0}: {1} ({2})", searchProperty.AttributeName, searchProperty.AttributeValue, searchProperty.ExactMatch ? "exact" : "contains")).ToList(); result.AddRange(_searchTags.Select(searchTag => string.Format("tag: {0}", searchTag))); if (_xPath != null) { result.Add(string.Format("XPath: {0}", _xPath)); } if (_textSearchData != null) { result.Add(string.Format("text: {0} ({1})", _textSearchData.Text, _textSearchData.ExactMatch ? "exact" : "contains")); } return string.Join(", ", result); } } }
      
      





ほとんどの関数は公開されており、開発者はテストで要素を説明します。 ほとんどすべての基準は、発生(exactMatch)で検索する機能を提供します。 ご覧のとおり、最終的にはすべてXPathに帰着します(XPathが通常の検索よりも少し遅くなることは除外しませんが、個人的にはこれに気付きませんでした)。



WebElementExceptions.cs


 namespace Autotests.Utilities.WebElement { public class WebElementNotFoundException : Exception { public WebElementNotFoundException(string message) : base(message) { } } }
      
      





さて、カスタム例外は1つだけです。



WebElementFilters.cs


 namespace Autotests.Utilities.WebElement { public partial class WebElement { private IEnumerable<IWebElement> FilterByVisibility(IEnumerable<IWebElement> result) { return !_searchHidden ? result.Where(item => item.Displayed) : result; } private IEnumerable<IWebElement> FilterByTagNames(IEnumerable<IWebElement> elements) { return _searchTags.Aggregate(elements, (current, tag) => current.Where(item => item.TagName == EnumHelper.GetEnumDescription(tag))); } private IEnumerable<IWebElement> FilterByText(IEnumerable<IWebElement> result) { if (_textSearchData != null) { result = _textSearchData.ExactMatch ? result.Where(item => item.Text == _textSearchData.Text) : result.Where(item => item.Text.Contains(_textSearchData.Text, StringComparison.InvariantCultureIgnoreCase)); } return result; } private IEnumerable<IWebElement> FilterByTagAttributes(IEnumerable<IWebElement> elements) { return _searchProperties.Aggregate(elements, FilterByTagAttribute); } private static IEnumerable<IWebElement> FilterByTagAttribute(IEnumerable<IWebElement> elements, SearchProperty searchProperty) { return searchProperty.ExactMatch ? elements.Where(item => item.GetAttribute(searchProperty.AttributeName) != null && item.GetAttribute(searchProperty.AttributeName).Equals(searchProperty.AttributeValue)) : elements.Where(item => item.GetAttribute(searchProperty.AttributeName) != null && item.GetAttribute(searchProperty.AttributeName).Contains(searchProperty.AttributeValue)); } } }
      
      





FindIWebElements(WebElement.csファイル)でアイテムをフィルターするために呼び出されるフィルター。 大規模なデータセットでは、Linqがforおよびforeachよりもはるかに長く機能するため、クラシックループを使用してこのコードを書き直すことは理にかなっていることに注意してください。



おわりに


記事で作成された記事のエラーと、コメントの質問が再び表示されます。



備考


-この記事では、enum、EnumHelper、Executorのコードは提供していません。 最後の部分に完全なコードを投稿します

-使用されるstring.Containsメソッドは拡張機能です:

 public static bool Contains(this string source, string target, StringComparison stringComparison) { return source.IndexOf(target, stringComparison) >= 0; }
      
      






All Articles