Добрый день. Недавно мы рассмотрели использование паттерна Page Object и селениум, но решил пойти немного далее, так как предыдущая версия не совсем удачная, – в ней много повторяемостей в коде. Это приводит к нарушению принципов SOLID. Таким образом, нашей целью будет создание такого ООП проекта, который будет лимитировать переиспользование кода.
Начну с основного класса Driver, в котором будут объявлятся свойства для будущей инициализации браузера, ожидания, а также методы запуска и закрытия браузера (разрушения вебдрайвера).
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
using OpenQA.Selenium; using OpenQA.Selenium.Firefox; using OpenQA.Selenium.Support.UI; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AdvancedPOM { public static class Driver { private static WebDriverWait browserWait; private static IWebDriver browser; public static IWebDriver Browser { get { if (browser == null) { throw new NullReferenceException("The WebDriver browser instance was not initialized. You should first call the method Start."); } return browser; } private set { browser = value; } } public static WebDriverWait BrowserWait { get { if (browserWait == null || browser == null) { throw new NullReferenceException("The WebDriver browser wait instance was not initialized. You should first call the method Start."); } return browserWait; } private set { browserWait = value; } } public static void StartBrowser(BrowserTypes browserType = BrowserTypes.Firefox, int defaultTimeOut = 30) { switch (browserType) { case BrowserTypes.Firefox: Driver.Browser = new FirefoxDriver(); break; case BrowserTypes.InternetExplorer: break; case BrowserTypes.Chrome: break; default: break; } BrowserWait = new WebDriverWait(Driver.Browser, TimeSpan.FromSeconds(defaultTimeOut)); } public static void StopBrowser() { Browser.Quit(); Browser = null; BrowserWait = null; } } } |
Список браузеров задается в перечислении:
|
1 2 3 4 5 6 7 8 9 10 |
namespace AdvancedPOM { public enum BrowserTypes { Firefox, InternetExplorer, Chrome, NotSet } } |
Давайте обратимся к предыдущей версии кода:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
using OpenQA.Selenium; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace POMWithoutInitFactory { public class BingMainPageElementMap { private readonly IWebDriver browser; public BingMainPageElementMap(IWebDriver browser) { this.browser = browser; } public IWebElement SearchBox { get { return this.browser.FindElement(By.Id("sb_form_q")); } } public IWebElement GoButton { get { return this.browser.FindElement(By.Id("sb_form_go")); } } public IWebElement ResultsCountDiv { get { return this.browser.FindElement(By.Id("b_tween")); } } } } |
Что здесь не так?.. Основная проблема – это то, что для каждой страницы (класса) нам приходится передавать в конструктор текущий объект вебдрайвера. Мы можем сделать некоторый рефакторинг и создать класс BasePageElementMap, от которого будут наследоваться все остальные классы инициализации элементов.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using OpenQA.Selenium; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AdvancedPOM { public class BasePageElementMap { protected IWebDriver browser; public BasePageElementMap() { this.browser = Driver.Browser; } } } |
Теперь наш обновленный класс BingMainPageElementMap будет иметь вид:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
using OpenQA.Selenium; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AdvancedPOM { public class BingMainPageElementMap : BasePageElementMap { public IWebElement SearchBox { get { return this.browser.FindElement(By.Id("sb_form_q")); } } public IWebElement GoButton { get { return this.browser.FindElement(By.Id("sb_form_go")); } } public IWebElement ResultsCountDiv { get { return this.browser.FindElement(By.Id("b_tween")); } } } } |
Таким образом, мы вынесли повторяющийся конструктор в базовый класс.
Следующим шагом будет создание базового класса для валидации.
Предыдущий класс имел следующий вид:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenQA.Selenium; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace POMWithoutInitFactory { class BingMainPageValidator { private readonly IWebDriver browser; public BingMainPageValidator(IWebDriver browser) { this.browser = browser; } protected BingMainPageElementMap Map { get { return new BingMainPageElementMap(this.browser); } } public void ResultsCount(string expectedCount) { Assert.IsTrue(this.Map.ResultsCountDiv.Text.Contains(expectedCount), "The results DIV doesn't contains the specified text."); } } } |
Здесь мы можем переместить свойство Map и конструктор в отдельный базовый класс.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenQA.Selenium; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AdvancedPOM { public class BasePageValidator<M> where M : BasePageElementMap, new() { protected M Map { get { return new M(); } } } } |
С помощью этого Generic класса у нас есть доступ к классу определения элементов.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AdvancedPOM { public class BingMainPageValidator : BasePageValidator<BingMainPageElementMap> { public void ResultsCount(string expectedCount) { Assert.IsTrue(this.Map.ResultsCountDiv.Text.Contains(expectedCount), "The results DIV doesn't contains the specified text."); } } } |
Финальный шаг в процессе рефакторинга паттерна Page Object – это создание базового класса для вышеупомянутых классов. Раньше он имел вид:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
using OpenQA.Selenium; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace POMWithoutInitFactory { class BingMainPage { private readonly IWebDriver browser; private readonly string url = @"http://www.bing.com/"; public BingMainPage(IWebDriver browser) { this.browser = browser; } protected BingMainPageElementMap Map { get { return new BingMainPageElementMap(this.browser); } } public BingMainPageValidator Validate() { return new BingMainPageValidator(this.browser); } public void Navigate() { this.browser.Navigate().GoToUrl(this.url); } public void Search(string textToType) { this.Map.SearchBox.Clear(); this.Map.SearchBox.SendKeys(textToType); this.Map.GoButton.Click(); } } } |
Здесь есть несколько элементов, которые также можно вынести в отдельный класс: конструктор, методы Navigate, Validate и свойство Map. Для этого нужно прибегнуть к созданию двух дополнительных классов:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AdvancedPOM { public class BasePage<M> where M : BasePageElementMap, new() { protected readonly string url; public BasePage(string url) { this.url = url; } protected M Map { get { return new M(); } } public void Navigate() { Driver.Browser.Navigate().GoToUrl(this.url); } } public class BasePage<M, V> : BasePage<M> where M : BasePageElementMap, new() where V : BasePageValidator<M>, new() { public BasePage(string url) : base(url) { } public V Validate() { return new V(); } } } |
Таким образом, имея эти два класса с параметрами и конструкторы, мы можем использовать их для любого класса страниц.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AdvancedPOM { public class BingMainPage : BasePage<BingMainPageElementMap, BingMainPageValidator> { public BingMainPage() : base(@"http://www.bing.com/") { } public void Search(string textToType) { this.Map.SearchBox.Clear(); this.Map.SearchBox.SendKeys(textToType); this.Map.GoButton.Click(); } } } |
Теперь класс BingMainPage состоит только с одного конструктора и метода Search, весь код бизнес-логики спрятан в базовых классах.
Ну, и сам тестовый класс, который остается практически без изменения:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Threading; namespace AdvancedPOM { [TestClass] public class AdvancedBingTests { [TestInitialize] public void SetupTest() { Driver.StartBrowser(); } [TestCleanup] public void TeardownTest() { Driver.StopBrowser(); } [TestMethod] public void SearchTextInBing_Advanced_PageObjectPattern() { BingMainPage bingMainPage = new BingMainPage(); bingMainPage.Navigate(); bingMainPage.Search("автоматизированное тестирование"); bingMainPage.Validate().ResultsCount("РЕЗУЛЬТАТЫ: 20,600"); } } } |
Окончательный проект в солюшене должен выглядеть так:
