Многие тест автоматизаторы рано или поздно сталкиваются с вопросом проектирования логов в их новых фреймворках. Остро проблема встает когда речь идет об уже существующих тестовых продуктах. Имея уже устоявшуюся архитектуру и тысячи строк кода, внедрение эффективного логирования могло бы показаться нерешаемой задачей.
К счастью, Selenium предоставляет удобный и эффективный инструмент для покрытия даже легаси-кода довольно гибкой системой логов. Говоря точнее, этот инструмент позволяет добавлять логирование практически любых действий, которые ваш тестовый код может произвести с объектами реализующими интерфейсы WebDriver либо WebElement. В сегодняшней статье мы смоделируем такой "легаси-фреймворк" и добавим некоторую функциональность логирования в него.
Описание примера: логируем текст и делаем скриншот для каждого шага в Selenium
В статье мы смоделируем легаси-фреймворк для тестирования пользовательского интерфейса с использованием Selenium. Модель будет состоять из одного единственного класса, но эта модель легко масштабируется на сколь угодно сложные и сколь угодно большие тестовые фреймворки. Такие фреймворки обычно имеют некоторый единый механизм, создающий объект вебдрайвера, используемый далее повсеместно. Наша задача состоит в том, чтобы добавить в такой фреймворк логирование необходимых событий, не затрагивая сам тестовый код. Итак, вот наша модель:
package click.webelement.logging; import org.junit.jupiter.api.*; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.firefox.FirefoxDriver; public class LegacyTest { WebDriver driver; @BeforeAll public static void globalSetup(){ System.setProperty("webdriver.gecko.driver", "/home/alexey/Desktop/Dev/webdrivers/geckodriver"); } @BeforeEach public void setUp(){ driver = new FirefoxDriver(); } @Test @DisplayName("WebElement.Click demo legacy tests") public void testLogging(){ driver.get("https://webelement.click/en/welcome"); driver.findElement(By.xpath("//a[text()='About Me']")).click(); } @AfterEach public void tearDown(){ if(driver != null){ driver.quit(); } } }
Для демонстрации работы принципа, мы выберем следующие действия, к которым привяжем наши лог-сообщения: driver.get
, driver.findElement
и WebElement.click
. Реальные возможности такого подхода гораздо шире, но для краткости, мы рассмотрим только три вышеупомянутых действия. Мы добавим текстовое логирование к методам, относящимся к вебдрайверу, и снимки экрана для методов вебэлементов. Например, мы будем делать снимок экрана до и снимок экрана после каждого клика по вебэлементу.
EventFiringWebDriver как стандарт подхода к логированию в Selenium Java
Клиентская Java-библиотека Selenium предоставляет класс EventFiringWebDriver
, являющийся "обёрткой" для обычного вебдрайвера и действующего как декоратор. Для некоторых методов, он оборачивает вызовы некоторой логикой, исполняющейся до вызова, и некоторой логикой, исполняющейся после. Такой подход реализуется при помощи т.н. листенеров. Также этот класс декорирует объекты WebElement, возвращаемые методами findElement
и findElements
, при условии их вызова через "обёртку". Элементы и вебдрайвер используют один и тот же листенер, который, в свою очередь, должен реализовывать интерфейс WebDriverEventListener
.
Пока что всё выглядит довольно запутанно. Чуть позже мы взглянем на это более пристально и всё прояснится.
И так, пока что мы можем сказать, что для того, чтобы достичь нашей цели, мы должны:
-
Каким-то образом реализовать листенер, который бы содержал код вызываемый до и после некоторых методов вебдрайвера и вебэлементов. Код бы логировал текстовые сообщения и делал бы снимки экрана.
-
Обернуть существующий WebDriver в
EventFiringWebDriver
и зарегистрировать в нем наш листернер (наши листенеры) -
Каким-то образом подменить существующий WebDriver, используемый в легаси-фреймворке, на нашу обёртку.
Возможно вы обратили внимание на то, что мы можем зарегистрировать одновременно несколько листенеров, которые, в свою очередь, могут определять код обработки для одних и тех же действий. Основное правило такое: при наступлении действия, для которого определена логика обработки в нескольких листенерах одновременно, такая логика вызывается в порядке в котором листенеры были зарегистрированы.
Список поддерживаемых действий можно почерпнуть из списка методов, определяемых интерфейсом WebDriverEventListener
. Интерфейс определяет 13 пар методов "before"/"after" как для действий, относящихся к WebDriver, так и для действий, относящихся к WebElement, плюс один метод, определяющий реакцию на возникающие ексепшены.
Реализуем WebDriverEventListener с нашими кастомными обработчиками
Давайте вспомним, что конкретно мы бы хотели логировать в нашем легаси-фрейморке.
-
Логировать навигацию к странице, выполняемую с использованием либо метода
driver.navigate()
либо методаdriver.get()
. В таком случае мы будем создавать текстовую запись и делать снимок экрана после того как навигация завершилась. -
Логировать попытки отыскать элемент через
driver.findElement
. В таком случае мы будем создавать текстовую запись и делать снимок экрана до того как поиск элемента начинается. -
Логировать попытки кликнуть на элемент. В таком случае мы будем создавать текстовую запись и делать скриншот до и после того как клик будет исполнен.
Давайте еще раз взглянем на интерфейс WebDriverEventListener
. Ниже я покажу методы, которые нам потребуется реализовать:
void afterNavigateTo(String url, WebDriver driver); void beforeFindBy(By by, WebElement element, WebDriver driver); void beforeClickOn(WebElement element, WebDriver driver); void afterClickOn(WebElement element, WebDriver driver);
Проблема в том, что мы не можем реализовать только какую-то часть методов, определяемых интерфейсом, оставив другую часть нереализованной. Java просто не работает так. К счастью, разработчики Selenium подготовили имплементацию интерфейса WebDriverEventListener
"по-умолчанию". Такая реализация хранится в абстрактном классе AbstractWebDriverEventListener
, реализация методов в котором не содержит полезного кода (тело каждого метода состоит только из пустой строки). Также, из-за того, что этот класс абстрактный, мы не можем создавать его экземпляры напрямую (опять же из-за того что Java так работает).
Подход, который для нас подготовили разработчики Selenium заключается в том, что мы создаем расширение существующего абстрактного класса, в котором мы перегружаем только те методы, которые нас интересуют. Это помогает экономить и время и объем кода. Итак, давайте добавим следующий класс в наш проект:
package click.webelement.logging; import org.openqa.selenium.*; import org.openqa.selenium.support.events.AbstractWebDriverEventListener; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.FileChannel; import java.util.UUID; public class CustomLoggingListener extends AbstractWebDriverEventListener { private static final String SCREENSHOT_LOCATION = "/home/alexey/Desktop/Dev/testing/screenshots"; @Override public void afterNavigateTo(String url, WebDriver driver) { String messageId = UUID.randomUUID().toString(); System.out.println(messageId + " : Navigating to [" + url + "] with driver [" + driver + "]"); takeScreenShot(messageId, driver); } @Override public void beforeFindBy(By by, WebElement element, WebDriver driver) { String messageId = UUID.randomUUID().toString(); System.out.println(messageId + " : Try to locate element using [" + by + "] and driver [" + driver + "] and element [" + element + "]"); takeScreenShot(messageId, driver); } @Override public void beforeClickOn(WebElement element, WebDriver driver) { String messageId = UUID.randomUUID().toString(); System.out.println(messageId + " : Clicking element [" + element + "] with driver [" + driver + "]"); takeScreenShot(messageId, driver); } @Override public void afterClickOn(WebElement element, WebDriver driver) { String messageId = UUID.randomUUID().toString(); System.out.println(messageId + " : Clicked element [" + element + "] with driver [" + driver + "]"); takeScreenShot(messageId, driver); } private void takeScreenShot(String name, WebDriver driver){ File src = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE); try { FileChannel srcChannel = new FileInputStream(src).getChannel(); File dst = new File(SCREENSHOT_LOCATION, name + ".png"); FileChannel dstChannel = new FileOutputStream(dst).getChannel(); dstChannel.transferFrom(srcChannel, 0, srcChannel.size()); } catch (IOException e) { e.printStackTrace(); } } }
Тут я должен дать некоторые пояснения. Прежде всего, конечно, вам понадобится поменять значение SCREENSHOT_LOCATION
на путь, подходящий для вашего окружения.
Еще одна вещь, на которую следует обратить внимание - это метод takeScreenShot
. Он принимает имя файла в качестве параметра (заметьте, что расширение указывать не требуется), добавляет к нему .png
и делает снимок экрана (в том случае, если ваш WebDriver поддерживает такую функциональность), сохраняя его в указанный файл. Более в методе нет каких-либо достойных внимания вещей. Всё достаточно стандартно.
Далее двигаемся к "перегруженным" методам (те, что сопровождаются аннотацией @Override
). Прежде всего я логирую текстовые сообщения при помощи стандартного вывода (System.out.println()
), что выглядит не очень красиво, но что делает пример проще. Следующее, что стоит отметить - это ограничения на используемые символы при создании файлов. По этой причине я не включаю никакие части свойств самого вебдрайвера или вебэлементов в имена файлов, создаваемых для хранения снимков экрана. Вместо этого, я генерирую случайную строку для каждого лог-сообщения. Используя такой "ключ", сопоставить созданный скриншот с произошедшим в тесте событием не составляет никакого труда.
Интегрируем созданные листенеры в наши легаси-тесты
Нам остается добавить последний штрих. А именно подменить существующую ссылку на вебдрайвер, используемую в тестах, на нашу EventFiringWebDriver
обёртку. Это очень просто. Так как EventFiringWebDriver
реализует интерфейс WebDriver
мы можем просто переприсвоить новое значение существующему полю. В нашем примере, нам просто потребуется внести незначительные изменения в метод setUp()
:
Вместо этого:
@BeforeEach public void setUp(){ driver = new FirefoxDriver(); }
У нас в итоге будет вот это:
@BeforeEach public void setUp(){ driver = new FirefoxDriver(); EventFiringWebDriver eventFiringWebDriver = new EventFiringWebDriver(driver); eventFiringWebDriver.register(new CustomLoggingListener()); driver = eventFiringWebDriver; }
Итак что мы только что сделали? Добавив один небольшой класс и три строчки в процедуру создания экземпляра нашего драйвера, мы реализовали логирование в тестах, не изменив ни строчки кода самих тестов. При этом получив максимальную пользу при минимальном рефакторинге.
Если у вас остались вопросы, задавайте их тут. Я постараюсь дополнить статью опираясь на ваши замечания.