Статьи

Расширенный шаблон объекта страницы – шаблоны проектирования в тестировании автоматизации

В моем последнем посте « Шаблон объекта страницы » из серии « Шаблоны проектирования в тестировании автоматизации » я привел вам примеры использования одного из самых популярных шаблонов в веб-автоматизации для шаблона объекта страницы. В этой статье я собираюсь в дальнейшем улучшить основную идею паттерна.

Если вы прочтете код из моего последнего поста, вы, скорее всего, заметите, что он тормозит один из самых важных принципов программирования —  СУХОЙ ( Не повторяйте себя ). Основной целью улучшений шаблона будет создание ООП-дизайна классов для ограничения повторения блоков кода.

Базовый класс WebDriver

Основная цель класса ниже — предоставить прямой способ инициализации и доступа к экземпляру веб-драйвера.

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;
    }
}

Драйвер инициализируется с помощью  метода StartBrowser,  где клиент может установить определенный тип браузера и время ожидания. Остановка браузера, которая обычно выполняется в TestCleanup, также является простой задачей с помощью статического метода StopBrowser . Если клиент пытается получить доступ к экземпляру до инициализации, генерируется исключение.

Расширенный шаблон объекта страницы ООП Дизайн

Первый класс, который нуждается в улучшении — это карта элементов.

Первая версия

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"));
        }
    }
}

Основная проблема здесь заключается в том, что каждый клиент класса должен передать своему конструктору текущий экземпляр Web Driver. Мы можем сделать это лучше с помощью ранее созданного статического класса Driver . Мы можем создать карту базовых элементов, которую будут получать все остальные карты элементов.

public class BasePageElementMap
{
    protected IWebDriver browser;

    public BasePageElementMap()
    {
        this.browser = Driver.Browser;
    }
}

Улучшенная версия

public class BingMainPageElementMap : BasePageElementMap
{
    public IWebElement SearchBox 
    {
        get
        {
            return this.browser.FindElement(By.Id("sb_form_q"));
        }
    }
}

As you can see in the new version, the class doesn’t have a constructor. So this code block is not going to be repeated in all other map classes.

The next step in the improving process is to create a base class for the validator classes.

First Version

public 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.");
    }
}

In the first version of the class, the DRY design principle is again not followed. The Map property and the constructor need to be placed in every validator class.

Improved Version

public class BasePageValidator<M>
    where M : BasePageElementMap, new()
{
    protected M Map
    {
        get
        {
            return new M();
        }
    }
}

When derived this generic class is going to provide direct access to the element map.

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.");
    }
}

After the refactoring, we end up with more readable and cleaner solution.

The final step in the process of making a better page object pattern OOP design is to create a base class for all page classes.

First Version

public 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();
    }
}

There is a couple of items in the above class that is going to be repeated for every page class- the constructor, the Navigate method, the Validate method, and the Map property. As you can imagine, this is a lot of boilerplate code. In order to fix this problem, we can create the following two base classes.

namespace PatternsInAutomation.Tests.Advanced.Core
{
    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();
        }
    }
}

The first one can be derived if you want to have a page without validator. The second one extends the first and adds to it the Validate method. Via the generic parameters and their constraints, we can use these classes for all of our pages.

Improved Version

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();
    }
}

Now the BingMainPage class consists only of a single constructor and the Search method, all of the boilerplate code is moved to the base class.

 Advanced Page Object Pattern- Usage in Tests

[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("Automate The Planet");
        bingMainPage.Validate().ResultsCount(",000 RESULTS");
    }
}

If you compare this sample usage with the one presented in the previous solution, you will notice that they are completely identical.

There is also another benefit that comes from the usage of the BasePage object- you cannot use the Map property directly in your tests. In my opinion, the direct usage of element maps in the tests is not a good practice because it breaks the DRY principle.

You can download the full source code from my Github repository- https://github.com/angelovstanton/Projects/tree/master/PatternsInAutomation.Tests

If you like the article, please hit these share buttons. Thank you!

Next, you can found out even more about the Page Object Pattern in my next publication- Advanced Page Object Pattern – Design Patterns in Automation Testing

References: