Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Проект Selenium WebDriver


Автор:Simon Stewart
Перевод: Н.Ромоданов

16.4. Побеждаем сложность

Программа является конструкцией, состоящей из отдельных частей. Отдельные части являются сложными, и мы, как разработчики интерфейса API, должные выбирать, как справляться с этой сложностью. С одной стороны, мы могли бы как можно равномернее распределить эту сложность по всему интерфейсу, а это означало бы, что с этой сложностью столкнется каждый, кто пользуется интерфейсом API. Другая крайность предполагает все, что связанно со сложностью, максимально собрать в одном месте. Это единственное место будет местом тьмы и ужаса для тех, кто рискнет им воспользоваться, но компромисс будет в том, что для пользователей API, которым не нужно вникать в реализацию, проблема сложности будет заранее решена.

Разработчики WebDriver больше склоняются к поиску и изоляции сложности в небольшом количестве мест, а не распределению ее по всему проекту. Одна из причин этого связана с нашими пользователями. Как видно по списку наших ошибок, пользователи исключительно хорошо находят проблемы и задают вопросы, но поскольку многие из них не являются разработчиками, сложный интерфейс API не будет способствовать хорошей работе. Мы стремились разрабатывать интерфейс API, который помогает людям двигаться в правильном направлении. В качестве примера, рассмотрим следующие методы из исходного интерфейса Selenium API, каждым из которых можно пользоваться при установке значений вводимых элементов:

  • type
  • typeKeys
  • typeKeysNative
  • keydown
  • keypress
  • keyup
  • keydownNative
  • keypressNative
  • keyupNative
  • attachFile

В WebDriver API их эквивалент следующий:

  • sendKeys

Как уже ранее обсуждалось, здесь отражено одно из основных философских различий между RC и WebDriver, состоящее в том, что WebDriver стремится подражать пользователям, тогда как в RC предлагаются интерфейсы API, функционирующие на более низком уровне, к которым, по мнению пользователя, сложно или невозможно получить доступ. Различие между методами typeKeys и typeKeysNative в том, что в первом случае всегда используются синтезируемые события, а во втором для ввода значений нажатых клавиш делаются попытки использовать AWT Robot. К сожалению, AWT Robot посылает информацию о нажатии клавиш в окно, имеющее фокус, которое не обязательно может быть браузером. Нативные события в WebDriver, напротив, направляются непосредственно в дескриптор окна, что позволяет не требовать от окна браузера, чтобы оно имело фокус.

16.4.1. Схема WebDriver

Команда разработчиков считает интерфейс WebDriver API «объектно-ориентированным2. Интерфейсы строго определены и мы стараемся, чтобы у каждого из них была только одна роль или функция, но вместе того, чтобы моделировать каждой отдельно взятый тег HTML в виде отдельного класса, у нас есть только один интерфейс WebElement. В соответствие с этим подходом, разработчики, которые используют IDE, поддерживающее автозавершение, могут переходить к выполнению следующего шага. В результате этого, кодирование может выглядеть (для языка Java) следующим образом:

WebDriver driver = new FirefoxDriver();
driver.<пользователь нажимает на пробел>

В этот момент появляется относительно короткий список из 13 методов, из которых нужно сделать выбор. Пользователь выбирает один из них:

driver.findElement(<пользователь нажимает на пробел>)

В большинстве IDE в это момент выпадает подсказка о типе ожидаемого аргумента, в нашем случае - объекты By. Для объектов By есть целый ряд предварительно сконфигурированных методов factory, которые для самого объекта By объявлены как статические методы. Наш пользователь быстро завершит ввод строки кода, которая будет выглядеть следующим образом:

driver.findElement(By.id("некоторое_id"));

Ролевые интерфейсы

Рассмотрим упрощенный класс Shop (Магазин). Каждый день в магазин должны поступать товары и, поэтому, он взаимодействует с классом Stockist (Поставщик), который нужен для поставки новой партии товара. Каждый месяц нужно платить сотрудникам и оплачивать налоги. Давайте предположим, что это делается с помощью класса Accountant (Бухгалтер). Один из способов моделирования этой ситуации выглядит следующим образом:

public interface Shop {
    void addStock(StockItem item, int quantity);
    Money getSalesTotal(Date startDate, Date endDate);
}

У нас есть два вспособа, как задать границу при определении интерфейса между магазином Shop, бухгалтером Accountant и поставщиком Stockist. Теоретически мы могли бы провести линию так, как показано на рис.16.1.

Это означает, что классы Accountant и Stockist должны восприниматься классом Shop как аргументы с их соответствующими методами. Однако недостатком здесь является то, что маловероятно, что бухгалтеру действительно нужны полки с товарами, и, вероятно, не лучшая идея реализовывать в классе Stockist огромные наценки на стоимость товаров, которые делаются в магазине. Таким образом, лучше нарисовать линию так, как это показано на рис.16.2.

Нам понадобится два интерфейса, которые нужно реализовать в классе Shop, но в этих интерфейсах точно определяется роль, которую выполняет класс Shop (Магазин) для обоих классов Accountant (Бухгалтер) и Stockist (Поставщик). Это следующие ролевые интерфейсы:

public interface HasBalance {
    Money getSalesTotal(Date startDate, Date endDate);
}

public interface Stockable {
    void addStock(StockItem item, int quantity);
}

public interface Shop extends HasBalance, Stockable {
}

Я считаю, что исключительные состояния UnsupportedOperationExceptions и иже с ними глубоко неприятны, но должно быть что-то, что позволяет использовать эти функции некоторой группе пользователей, которым это, возможно, нужно, и не загромождает остальные интерфейсы API для большинства пользователей. Для этого в WebDriver широко используется ролевой интерфейс. Например, есть интерфейс JavascriptExecutor, который в контексте текущей страницы позволяет выполнять произвольные куски кода на Javascript. Успешное приведение экземпляра WebDriver к этому интерфейсу указывает, что вы можете в работе использовать эти методы.

Рис.16.1: Классы Accountant (Бухгалтер) и Stockist (Поставщик) зависят от класса Shop (Магазин)

Рис.16.2: В классе Shop (Магазин) реализованы интерфейсы HasBalance (Получить баланс) и Stockable (Поступление товаров)

16.4.2. Комбинаторный взрыв

Первое, что сразу приходит на ум при мысли об огромном количестве браузеров и языков, которые поддерживает WebDriver, что если не принять меры, можно быстро столкнуться с нарастающими затратами его поддержки. При наличии X браузеров и Y языков очень легко попасть в ловушку, где требуется поддерживать X × Y реализаций.

Одним из способов уменьшения этих затрат могло бы быть уменьшение количества языков, которые поддерживает WebDriver, но мы не хотели идти по этому пути по двум причинам. Во-первых, когда переходишь с одного языка на другой, то это происходит за счет определенной когнитивной нагрузки, так что поскольку пользователей фреймворка могут писать свои тесты на том же самом языке, на котором они выполняют большую часть своей работы, у них имеется преимущество. Во-вторых, когда в одном проекте смешиваются несколько языков, команда разработчиков может чувствовать себя некомфортно и часто случается, что корпоративные стандарты кодирования и требования проектирования обязывают придерживаться технологического единообразия (хотя, я думаю, приятно то, что со временем эта вторая причина становится менее значимой), поэтому недопустимо уменьшать количество поддерживаемых языков.

Сокращение числа поддерживаемых браузеров так же не является вариантом — было много крика, когда мы прекратили в WebDriver поддержку Firefox 2, несмотря на то, что, когда мы так поступили, этот браузер занимал менее 1% рынка браузеров.

Единственное, что нам осталось выбрать, это сделать так, чтобы все браузеры с точки зрения привязки к языкам выглядели идентично: нужно предложить единый интерфейс, который мог бы подходить для самых различных языков. И еще мы хотели, чтобы привязки к языкам было бы настолько просто писать, насколько это возможно, что предполагало, что мы хотим сделать настолько тонкими, насколько это возможно. Чтобы поддержать такой подход, мы перенесли в драйвер, лежащий ниже, столько логики, сколько было возможным: то, что мы не могли перенести в драйвер, это те функциональные возможности, которые нужно реализовывать в каждом языке, который мы поддерживаем, и это может потребовать существенного объема работ.

Например, в драйвере IE функции, касающиеся местонахождения и запуска IE, были успешно перенесены в логику основного драйвера. Хотя это и удивило нас тем количеством строк кода, которое стало в драйвере, привязка к языку при создании нового экземпляра драйвера свелась к единственному вызову метода, находящегося в этом драйвере. Для сравнения, в драйвере Firefox такие изменения сделать не удалось. Только для языка Java это означает, что у нас есть три основных класса, занимающихся конфигурированием и запуском Firefox, размер которых равен приблизительно1300 строкам кода. Эти классы дублируются для каждой привязки к языку, в котором нужно поддерживать FirefoxDriver и следует полагаться на запуск сервера Java. В результате это свелось к сопровождению большого количества кода.

16.4.3. Недостатки схемы, используемой в WebDriver

Недостаток решения предоставлять так возможности драйвера заключается в том, что до тех пор, пока пользователи не знают, что существует конкретный интерфейс, они не смогут догадаться, что WebDriver поддерживает функциональные возможности такого рода: в API нет возможность самостоятельно узнать об этих возможностях. Конечно, когда WebDriver был новинкой, нам казалось, что мы потратим много времени, просто рассказывая о конкретных интерфейсах. Теперь мы тратим существенно больше усилий на нашу документацию и по мере того, как API используется все шире и шире, пользователям становится все легче и легче находить нужную им информацию.

В частности, есть место, где, по моему мнению, наш интерфейс API неудовлетворителен. У нас есть интерфейс, называемый RenderedWebElement, который представляет собой странное объединение методов, которые делают запросы обновленного состояния элемента (isDisplayed, getSize и getLocation)), выполняют с ним операции (метод hover и метод объекта захвата и его перетаскивания), и удобного метода, с помощью которого можно значение, установленного определенному свойству CSS. Интерфейс был создана, потому что в драйвере HtmlUnit необходимая информация не предоставлялась, а в драйверах Firefox и IE - предоставлялась. Первоначально в нем был только первый набор методов, но прежде, чем я успел подумать о том, как я бы хотел, чтобы этот API выглядел, мы добавили в него другие методы. Сейчас этот интерфейс хорошо известен и трудно решить, сохранять ли этот неприглядный уголок API, учитывая то, что он широко используется, или попытаться его удалить. Я предпочитаю не оставлять за собой "мусор", так что прежде, чем мы выпустим Selenium 2.0, важно решить эту проблему. В результате, к тому времени, когда вы будете читать статью, интерфейс RenderedWebElement, возможно, будет уже исправлен.

С точки зрения разработчиков, жесткое подключение к браузеру, хотя оно и неизбежно, также является конструктивным недостатком. Приходится предпринимать значительные усилия для того, чтобы поддерживать новый браузер, и зачастую для того, чтобы осуществить это правильно, нужно сделать нескольких попыток. Что касается конкретного примера, то драйвер Chrome прошел через четыре полных этапа переписывания, а драйвер IE также прошел через три основных этапа переписывания. Преимущество жесткой привязки к браузеру состоит в предоставлении больших возможностей управления.

Продолжение статьи - Уровни и Javascript.