Автоматизированное тестирование веб-приложения (MS Unit Testing Framework + Selenium WebDriver C#). Часть 2.1: Selenium API wrapper — Browser

  • Tutorial
Selenium + C#
Введение

Снова здравствуйте! Представляю вам вторую часть статьи на тему автоматизированного тестирования веб-приложения на Selenium и C#. И если первая часть была из разряда «капитан очевидность», что вызвало негодование у читателей, то в этой части будет много кода. И так, зачем же писать обертку (wrapper) над Selenium API? На мой взгляд, пользователи могут столкнуться со следующими проблемами:
  • Интерфейс IWebDriver предоставляет довольно скудную функциональность в плане управления браузером. Поэтому все, что нам понадобится, придется писать самим
  • Описание элемента происходит одновременно с его поиском, т.е. на момент определения элемента он должен существовать в браузере. Очень часто решается путем написания getter для каждого элемента. Это накладно и плохо с точки зрения производительности
  • ISearchContext.FindElements принимает только один параметр типа OpenQA.Selenium.By, т.е. мы не можем искать сразу по нескольким свойствам. Обычно элемент ищется по первому критерию, а затем начинается отсеивание по остальным
  • Отсутствие многих, казалось бы, очевидных методов и свойств. Например: Exist, SetText, Select, SetCheck, InnerHtml и т.д. Вместо этого мы вынуждены довольствоваться Click, SendKeys и Text
  • Множество проблем на различных браузерах, например на Firefox и Chrome элемент кликается, а на IE — нет. Приходится писать special cases, «костыли»
  • Производительность. Да, драйвера работают не быстро. Впереди планеты всей как обычно IE — поиск может занимать секунды, иногда и десятки секунд

Ну что ж, много проблем — много решений. Я не хочу и не смогу изложить все и сразу в этой части. Длинный пост будет неинтересен, да и во время его написания энтузиазм резко падает) Поэтому эта часть имеет номер 2.1, в ней я покажу свой wrapper над браузером.

Ссылки

Часть 1: Введение
Часть 2.1: Selenium API wrapper — Browser
Часть 2.2: Selenium API wrapper — WebElement
Часть 3: WebPages — описываем страницы
Часть 4: Наконец-то пишем тесты
Публикация фреймворка

Поехали. Реализация враппера над браузером: класс Browser

Я очень надеюсь, что приведенный код не покажется вам сложным.
В примере инкапсулирован весь специфический для IWebDriver код, трусы наружу не высовываются, т.е. разработчики автотестов не будут иметь прямого доступа к драйверу. Как следствие — наличие методов, в которых вызывается метод драйвера с таким же названием.
В коде нет комментариев — это хороший тон, и как сказал один умный чел: «Комментарии в коде это как волосы в супе. Ты бы стал есть суп с волосами?!»
Я использую Microsoft Code Contracts, не пугайтесь вызовов.
Еще стоит заметить, что в тестируемом продукте подключен jquery, и некоторые действия будут производиться с его использованием.

Класс Browser поддерживает 3 браузера:
  • Firefox
  • Chrome
  • Internet Explorer

Очень жаль, что пока нет C# драйверов для Opera :(

Реализован набор стандартных методов и свойств вроде:
  • Start
  • Quit
  • Navigate
  • NavigateBack
  • Refresh
  • WaitReadyState
  • FindElements
  • SelectedBrowser
  • Url
  • Title
  • PageSource
  • др.

Думаю, названия вполне очевидные, поэтому идем дальше.

Также реализованы специфические функции:
  • WaitAjax
  • SwitchToFrame
  • SwitchToPopupWindow
  • AcceptAlert
  • GetScreenshot + SaveScreenshot
  • ResizeWindow
  • ExecuteJavaScript
  • DragAndDrop
  • др.

namespace Autotests.Utilities
{
    [Serializable]
    public enum Browsers
    {
        [Description("Windows Internet Explorer")]
        InternetExplorer,
        
        [Description("Mozilla Firefox")]
        Firefox,

        [Description("Google Chrome")]
        Chrome
    }

    public static class Browser
    {
        #region Public properties

        public static Browsers SelectedBrowser
        {
            get { return Settings.Default.Browser; }
        }

        public static Uri Url
        {
            get { WaitAjax(); return new Uri(WebDriver.Url); }
        }

        public static string Title
        {
            get
            {
                WaitAjax();
                return string.Format("{0} - {1}", WebDriver.Title, EnumHelper.GetEnumDescription(SelectedBrowser));
            }
        }

        public static string PageSource
        {
            get { WaitAjax(); return WebDriver.PageSource; }
        }

        #endregion

        #region Public methods

        public static void Start()
        {
            _webDriver = StartWebDriver();
        }

        public static void Navigate(Uri url)
        {
            Contract.Requires(url != null);

            WebDriver.Navigate().GoToUrl(url);
        }

        public static void Quit()
        {
            if (_webDriver == null) return;

            _webDriver.Quit();
            _webDriver = null;
        }

        public static void WaitReadyState()
        {
            Contract.Assume(WebDriver != null);

            var ready = new Func<bool>(() => (bool)ExecuteJavaScript("return document.readyState == 'complete'"));

            Contract.Assert(Executor.SpinWait(ready, TimeSpan.FromSeconds(60), TimeSpan.FromMilliseconds(100)));
        }

        public static void WaitAjax()
        {
            Contract.Assume(WebDriver != null);

            var ready = new Func<bool>(() => (bool)ExecuteJavaScript("return (typeof($) === 'undefined') ? true : !$.active;"));

            Contract.Assert(Executor.SpinWait(ready, TimeSpan.FromSeconds(60), TimeSpan.FromMilliseconds(100)));
        }

        public static void SwitchToFrame(IWebElement inlineFrame)
        {
            WebDriver.SwitchTo().Frame(inlineFrame);
        }

        public static void SwitchToPopupWindow()
        {
            foreach (var handle in WebDriver.WindowHandles.Where(handle => handle != _mainWindowHandler)) // TODO:
            {
                WebDriver.SwitchTo().Window(handle);
            }
        }

        public static void SwitchToMainWindow()
        {
            WebDriver.SwitchTo().Window(_mainWindowHandler);
        }

        public static void SwitchToDefaultContent()
        {
            WebDriver.SwitchTo().DefaultContent();
        }

        public static void AcceptAlert()
        {
            var accept = Executor.MakeTry(() => WebDriver.SwitchTo().Alert().Accept());

            Executor.SpinWait(accept, TimeSpan.FromSeconds(5));
        }

        public static IEnumerable<IWebElement> FindElements(By selector)
        {
            Contract.Assume(WebDriver != null);

            return WebDriver.FindElements(selector);
        }

        public static Screenshot GetScreenshot()
        {
            WaitReadyState();

            return ((ITakesScreenshot)WebDriver).GetScreenshot();
        }

        public static void SaveScreenshot(string path)
        {
            Contract.Requires(!string.IsNullOrEmpty(path));

            GetScreenshot().SaveAsFile(path, ImageFormat.Jpeg);
        }

        public static void DragAndDrop(IWebElement source, IWebElement destination)
        {
            (new Actions(WebDriver)).DragAndDrop(source, destination).Build().Perform();
        }

        public static void ResizeWindow(int width, int height)
        {
            ExecuteJavaScript(string.Format("window.resizeTo({0}, {1});", width, height));
        }

        public static void NavigateBack()
        {
            WebDriver.Navigate().Back();
        }

        public static void Refresh()
        {
            WebDriver.Navigate().Refresh();
        }

        public static object ExecuteJavaScript(string javaScript, params object[] args)
        {
            var javaScriptExecutor = (IJavaScriptExecutor)WebDriver;

            return javaScriptExecutor.ExecuteScript(javaScript, args);
        }

        public static void KeyDown(string key)
        {
            new Actions(WebDriver).KeyDown(key);
        }

        public static void KeyUp(string key)
        {
            new Actions(WebDriver).KeyUp(key);
        }

        public static void AlertAccept()
        {
            Thread.Sleep(2000);
            WebDriver.SwitchTo().Alert().Accept();
            WebDriver.SwitchTo().DefaultContent();
        }

        #endregion

        #region Private

        private static IWebDriver _webDriver;
        private static string _mainWindowHandler;

        private static IWebDriver WebDriver
        {
            get { return _webDriver ?? StartWebDriver(); }
        }

        private static IWebDriver StartWebDriver()
        {
            Contract.Ensures(Contract.Result<IWebDriver>() != null);

            if (_webDriver != null) return _webDriver;

            switch (SelectedBrowser)
            {
                case Browsers.InternetExplorer:
                    _webDriver = StartInternetExplorer();
                    break;
                case Browsers.Firefox:
                    _webDriver = StartFirefox();
                    break;
                case Browsers.Chrome:
                    _webDriver = StartChrome();
                    break;
                default:
                    throw new Exception(string.Format("Unknown browser selected: {0}.", SelectedBrowser));
            }

            _webDriver.Manage().Window.Maximize();
            _mainWindowHandler = _webDriver.CurrentWindowHandle;

            return WebDriver;
        }

        private static InternetExplorerDriver StartInternetExplorer()
        {
            var internetExplorerOptions = new InternetExplorerOptions
                {
                    IntroduceInstabilityByIgnoringProtectedModeSettings = true,
                    InitialBrowserUrl = "about:blank",
                    EnableNativeEvents = true
                };

            return new InternetExplorerDriver(Directory.GetCurrentDirectory(), internetExplorerOptions);
        }

        private static FirefoxDriver StartFirefox()
        {
            var firefoxProfile = new FirefoxProfile
                {
                    AcceptUntrustedCertificates = true,
                    EnableNativeEvents = true
                };

            return new FirefoxDriver(firefoxProfile);
        }

        private static ChromeDriver StartChrome()
        {
            var chromeOptions = new ChromeOptions();
            var defaultDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\..\Local\Google\Chrome\User Data\Default";

            if (Directory.Exists(defaultDataFolder))
            {
                Executor.Try(() => DirectoryExtension.ForceDelete(defaultDataFolder));
            }

            return new ChromeDriver(Directory.GetCurrentDirectory(), chromeOptions);
        }

        #endregion
    }
}

Возможно стоит прокомментировать, что Settings.Default.Browser — это параметр, который задается в свойствах проекта, а Executor.SpinWait — некоторый хелпер, который дожидается выполнения условия с таймаутом и возвращает true/false. Так же встречаются magic strings, извиняюсь)
  • +15
  • 38,4k
  • 3
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 3
  • 0
    Спасибо за вторую часть.
    Надеюсь будет и продолжение, чтобы посмотреть на примере, с какими интересными тестами справляется ваша конфигурация (велосипед).
    Мне просто хочется понять, насколько она нужна, если большинство необходимых мне тестов сводится к вводу/редактированию данных на страничке, сохранению этой информации и просмотру отчётов. Грубо говоря, тестирования документооборота…
    • 0
      Сложность тестов зависит от сложности приложения (в данном случае — веб). С большим количеством AJAX вызовов, окнами, перекрывающими экран, и прочее прочее, без «допиливания» webdriver'а сложно обойтись.

      Стоит также упомянуть наличие багов и неочевидного поведения браузеров и их костыливание, что добавляет «велосипедности» решению.
      • 0
        Абсолютно верно. А когда в команде разработчиков автотестов несколько человек, лучше, когда все используют один велосипед, чем каждый придумывает свой.

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.