Ожидание событий в Selenium RC, часть 2 -- AJAX

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

Система команд в Selenium

Все команды, которые есть в Selenium, разделяются на три класса:

  • действия (actions)
  • получатели данных (accessors)
  • проверки (assertions)

Действия – это команды, которые управляют состоянием тестируемого приложения, такие как click, check, type, select, fireEvent и т.п.

Большая часть действий приводит к изменению состояния приложения – в результате выполнения такой команды либо отправляется запрос на сервер (например, проход по ссылке или отправка формы), либо происходят какие-то события в браузере (например, заполняются поля формы, устанавливаются cookies или отрабатывает javascript-функция).

Некоторые команды-действия сами ничего не делают, но управляют поведением других команд-действий, например setTimeout, answerOnNextPrompt, chooseCancelOnNextConfirmation.

Наконец, есть несколько команд, которые тоже почему-то относятся к действиям, но на самом деле это команды ожидания некоторого события. Это команды waitForCondition, waitForPageToLoad, waitForPopUp и waitForFrameToLoad, именно им была посящена предыдущая заметка (вообще-то команда waitForCondition может модифицировать состояние приложения, потому что в ней можно выполнить произвольный javascript-код, но теоретически она не должна иметь такого рода побочных эффектов).

Получатели данных – это команды, предназначенные для получения информации о состоянии тестируемого приложения. В Selenium IDE все такие команды начинаются со слова “store” – storeTitle, storeText, storeElementPresent и т.д., они сохраняют полученную информацию о состоянии приложения в переменные, которые могут быть использованы в последующих командах.

В Selenium RC используется другая схема именования – имена получателей, возвращающих текстовое значение, начинаются со слова “get”, а имена получателей, возвращающих булевское значение (да/нет, true/false), начинаются со слова “is”. Например, getTitle, getText, getAttribute, но – isChecked, isElementPresent, isVisible.

Проверки как самостоятельные команды существуют только в Selenium IDE. Они генерируются автоматически, для каждого получателя данных создается шесть проверок: прямая и обратная проверки в трёх режимах – assert, verify и waitFor. Например, для команды storeElementPresent создаются следующие проверки: assertElementPresent, assertElementNotPresent, verifyElementPresent, verifyElementNotPresent, waitForElementPresent, waitForElementNotPresent.

В Selenium RC проверки реализуются как комбинация получателя данных и подходящего метода из используемого фреймворка для разработки тестов.

Так, скажем, для фреймворка TestNG (Java) проверки типа “assert” будут выглядеть примерно так:

assertEquals(getText("id=result"), "expected value");
assertFalse(isElementPresent("id=error"));

А для фреймворка Python unittest аналогичные проверки будут такими:

self.assertEqual("expected value", sel.get_text("id=result"))
self.assertFalse(sel.is_element_present("id=error"))

Чуть сложнее устроены проверки типа “verify”. Они отличаются от проверок типа “assert” тем, что не должны немедленно прерывать выполнение теста, вместо этого сообщение об ошибке вносится в специальный список. Этот способ используется для выполнения некритичных проверок, после которых можно продолжать выполнение даже если проверка дала отрицательный результат. При этом тест отрабатывает до конца, и если список ошибок непустой, он всё-таки помечается как завершившийся неуспешно.

Тестовые фреймворки как правило не имеют встроенной поддержки для проверок такого типа.

Для тех, кто разрабатывает тесты на языке Java, ситуация несколько лучше. В TestNG проверки типа “verify” реализованы во вспомогательном классе SeleneseTestNgHelper, о котором мы уже говорили в предыдущих заметках. Выглядеть это будет следующим образом:

verifyEquals(getText("id=result"), "expected value");
verifyFalse(isElementPresent("id=error"));

Аналогичная поддержка проверок типа “verify” есть и в некоторых других фреймворках, в частности JUnit для Java и Groovy.

А вот для проверок типа “waitFor” нет поддержки ни в одном известном мне фреймворке или расширении для Selenium. Поэтому мы реализуем эту поддержку самостоятельно для TestNG (а если вы пользуетесь чем-нибудь другим – можете адаптировать это для своего фреймворка самостоятельно).

Но сначала ещё чуть-чуть поговорим о том, почему эти команды играют столь важную роль при тестировании AJAX-приложений

AJAX и команды-проверки типа “waitFor”

В “классических” веб-приложениях тесты устроены таким образом, что мы сначала выполняем некоторую последовательность действий, завершающуюся отправкой запроса на веб-сервер. Затем мы должны дождаться, пока браузер получит от сервера ответ, после чего приступить к его проверке. И для ожидания ответа обычно используется команда waitForPageToLoad.

Но для AJAX-приложений этот способ не годится, потому что обращения к серверу выполняются в “фоновом режиме”, после чего обновляются только отдельные части страницы, полностью страница не перегружается. Поэтому команда waitForPageToLoad оказывается совершенно бесполезной.

Вместо ожидания загрузки страницы в таких приложениях мы должны определить некоторые другие критерии завершения обработки запроса. Это может быть появление или исчезновение каких-либо элементов на странице, либо изменение их свойств – видимость, цвет, расположение и т.д. Соответственно, нам нужны команды для ожидания таких событий – а это и есть те самые команды-проверки типа “waitFor”, о которых шла речь выше.

Ну что ж, пришла пора заняться реализацией всех этих проверок.

Реализация waitFor-проверок

За основую релизации методов ожидания можно взять код, который генерирует Selenium IDE для проверок типа “waitFor”.

Вот что там предлагается, например, для команды waitForVisible:

for (int second = 0;; second++) {
  if (second >= 60) fail("timeout");
  try {
    if (selenium.isVisible("id=result")) break;
  } catch (Exception e) {}
  Thread.sleep(1000);
}

Идея вполне очевидна – в цикле раз в секунду проверять, виден ли нужный элемент. Если виден – ожидание прекращается. А если прошло уже достаточно много проверок (60) и все неуспешные – тогда можно завершить тест с сообщением о том, что время ожидания истекло.

Разумеется, невозможно каждый раз, когда требуется сделать такого рода проверку, вставлять столь громоздкий кусок кода. Давайте оформим его в виде вспомогательного метода, вот такого:

public void waitForVisible(String locator) {
  for (int second = 0;;  second++) {
    if (second >= 60) {
      throw new  AssertionError("timeout");
    }
    try {
      if (selenium.isVisible(locator))  break;
    } catch (Exception e) {}
    try {
      Thread.sleep(1000);
    } catch  (InterruptedException e) {
      throw new AssertionError(e);
    }
  }
}

Теперь посмотрим пристально, и попробуем понять, сколько времени будет ожидать этот метод, прежде чем сообщит о неудаче? Думаете, 60 секунд? Отнюдь! Например, на моём ноутбуке, где я пишу эту заметку, он работает примерно 140 секунд. Дело в том, что на самом деле в цикле считаются не секунды, а количество попыток. Между попытками проходит секунда, но сами попытки тоже требуют определённого времени, причём весьма существенного. То есть у меня 60 секунд ушло на ожидание, и ещё 80 секунд заняли обращения к Selenium.

Давайте исправим это так, чтобы метод на самом деле выполнял проверки в течение указанного времени:

public void waitForVisible(String locator) {
  long start = System.currentTimeMillis();
  while (System.currentTimeMillis() < start  + 60000) {
    try {
      if (selenium.isVisible(locator)) return;
    } catch  (Exception e) {}
    try {
      Thread.sleep(1000);
    } catch  (InterruptedException e) {
      throw new AssertionError(e);
    }
  }
  throw  new AssertionError("timeout");
}

Кроме того, хорошо бы сделать время ожидания параметром, а также дать возможность настраивать дефолтное время ожидания и промежуток между попытками:

public void waitForVisible(String locator) {
  waitForVisible(locator,  getDefaultTimeoutWaitFor());
}

public void waitForVisible(String  locator, long timeout) {
  long pause = getAttemptsInterval();
  long  start = System.currentTimeMillis();

  while (System.currentTimeMillis()  < start + timeout) {
    try {
      if (selenium.isVisible(locator)) return;
    } catch (Exception e) {}
    try {
      Thread.sleep(pause);
    } catch  (InterruptedException e) {
      throw new AssertionError(e);
    }
  }
  throw  new AssertionError("timeout");
}

Далее мы должны были бы создать аналогичные методы для всех команд получения данных, но это не очень хорошо, потому что у нас получится множество методов, похожих как близнецы-братья.

Более правильный способ состоит в том, чтобы отделить “логику ожидания” от “логики проверки”, создать один унивесальный метод ожидания, который может проверять разные условия. Именно так, кстати, реализованы проверки типа “assert” и “verify” – в них комбинируется единый универсальный метод проверки с семейством специализированных методов получения данных.

Мы сделаем такую реализацию, в которой проверка-ожидание будет выглядеть следующим образом:

boolean res = selenium.waitFor(Visible("id=result"));

То есть у нас будет унивесальный метод waitFor (а также waitForNot) и семейство методов, реализующих логику проверки, по одному для каждой операции получения данных.

Кроме того, мы сделаем так, чтобы при неуспешном завершении он не прерывал выполнение теста, а просто возвращал false (а при успешном завершении, соответственно, true). Это даст возможность разработчику тестов самостоятельно принять решение о том, что делать в той или иной ситуации. Если он решит, что тест должен прерываться, можно добиться этого эффекта путём комбинирования с методом assertTrue:

assertTrue(selenium.waitFor(Visible("id=result")));

Итак, вот как устроен универсальный метод ожидания, который мы поместим в класс WaitingSelenium:

public boolean waitFor(Condition condition) {
    return  waitFor(condition, getDefaultTimeoutWaitFor());
}

public boolean waitFor(Condition condition, long timeout) {
  long pause =  getAttemptsInterval();
  long start = System.currentTimeMillis();

  while (System.currentTimeMillis() < start + timeout) {
    try {
      if (condition.checkConditionWith(this)) return true;
    } catch (Exception e) {}
    try {
      Thread.sleep(pause);
    } catch (InterruptedException e) {
      return false;
    }
  }
  return false;
}

На вход он получает параметр типа Condition, это интерфейс, в котором имеется всего один метод:

public interface Condition {
  boolean checkConditionWith(Selenium selenium);
}

А вот метод Visible, который реализует проверку того, виден или нет элемент с заданным локатором:

public static Condition Visible(final String locator) {
  return new Condition() {
    public boolean checkConditionWith(Selenium selenium)  {
      return selenium.isVisible(locator);
    }
  };
}

Вот и всё. Теперь надо наделать много методов, аналогичных Visible – для всех команд получения данных, и можно пользоваться. Впрочем, всё это уже есть в приложенном архиве, содержащем код – в класс WaitingSelenium добавлены два универсальных метода ожидания, а в классе SeleneseTestNgHelper появилась целая серия методов, создающих проверки для практически всех команд-получателей данных. Пропущены команды getAllButtons, getAllFields, getAllLinks, getAllWindowIds, getAllWindowNames и getAllWindowTitles, для которых проверки типа waitFor не имеют особого смысла, но про которые мы ещё поговорим в будущем. Кроме того, нет проверок для команды storeLogMessages, которая просто заглушка без реализации, и для команд WhetherThisFrameMatchFrameExpression и WhetherThisWindowMatchFrameExpression, которые предназначены для сугубо служебных целей.

И напоследок ещё одно замечание – условия для проверки можно делать сколь угодно сложными, они не обязательно должны состоять только из одной команды Selenium.

А в следущей заметке серии мы реализуем ещё два метода ожидания, которых в Selenium нет вообще, но которые тоже бывают полезны при тестировании AJAX-приложений – waitForChange и waitForStopChanges.

В приложении находится проект Eclipse, содержащий исходный код расширения WaitingSelenium и модифицированный класс SeleneseTestNgHelper, в которых реализованы проверки-ожидания: WaitingSelenium2.zip