Часть 1
В предыдуших шагах мы позаботились, пожалуй о самой чувствительной к изменениям части - идентификаторах объектов. Но этого еще недостаточно, есть еще несколько шагов, которые нужно сделать, чтобы минимизировать затраты на поддержку, а также время на разработку, да и просто сделать тесты более понятными и удобными для чтения. Итак, рассмотрим эти шаги.
Шаг 4: Абстрагируемся до уровня действий на странице
В предыдущем шаге мы сумели вынести локаторы в отдельный файл и, задав, понятные имена, мы можем уже проследить, над какими элементами проводятся операции. Тем не менее, мы по-прежнему работаем на уровне примитивных команд, которые мало к чему привязаны. К тому же слабо прослеживается переход с одной страницы на другую.
Другой момент заключается в том, что держать в голове все псевдонимы всех элементов неудобно, особенно для больших объемов тестов. То есть надо бы как-то сделать так, чтобы и псевдонимы элементов использовались не так интенсивно. Да и хотелось бы, чтобы тестовые инструкции выглядели более информативно, например, не
``` selenium.clickAndWait( "leftpanel.newjob" );
</p>
<p>а что-то наподобие</p><p>
mainPage.clickOnNewJobLink();
</p><p>То есть примитивные операции обернуть в некоторый функционал, который уже отражал бы смысл операции. Это так называемый PageObject-подход, при котором некоторому отдельному окну/странице/форме соответствует некоторый класс, методы которого соответствуют либо каким-то дочерним элементам, либо примитивным действиям внутри данного окна/страницы/формы. Один из примеров
подобной реализации можно описан здесь: <a href="http://autotestgroup.com/ru/blog/55.html">http://autotestgroup.com/ru/blog/55.html</a>, а точнее реализация подобного для TestComplete. Там было описано, как обернуть некоторые дочерние элементы. В нашем случае применим подход обертки действий над некоторыми элементами, так как <a href="http://automated-testing.info/knowledgebase/avtomatizacija-funkcionalnogo-testirovanija/selenium" target="_blank" title="Статьи и уроки по Selenium">Selenium</a> больше ориентирован на действия, которые проводятся над объектом, а не на объекты, над которыми проводятся действия. Это достаточно тонкая грань, которую надо уметь усмотреть.</p>
<p>В
любом случае, нам нужен некоторый набор классов, которые могли бы
соответствовать некоторым страницам. Сразу следует обратить внимание на то, что
если мы скрываем действия Selenium-а внутри некоторого внешнего класса, то нам надо в этот класс передать
объект Selenium-а, созданный тестом. Например, при создании любого объекта страницы в
качестве параметра передается объект <a href="http://automated-testing.info/knowledgebase/article/chto-takoe-selenium-i-s-chem-ego-edjat" target="_blank" title="Авматизация на Selenium">Selenium</a>-a. Пожалуй, это будет наиболее общее для всех
объектов страниц решение. В пакет com.mycompany.selenium.lib добавим
класс BaseTestClass со следующим содержимым:</p><p>
/**
*
*/
package com.mycompany.selenium.lib;
/**
- @author KaNoN
*/
public class PageObjectClass {
protected ExtendedSelenium selenium = null;
public PageObjectClass( ExtendedSelenium selenium ) throws Exception {
this.selenium = selenium;
}
}
</p>
<p>После этого, мы можем создавать классы
страниц, которые (классы) будут отнаследованы от данного класса. Еще
один момент. Когда мы работаем с объектом страницы, то в ряде случаев, когда мы
делаем действие, приводящее к переходу на новую страницу, было бы полезно
возвращать объект этой новой страницы. Учитывая
эти пожелания, создадим отдельный пакет для классов страниц. Назовем его “<strong>com.</strong><strong>mycompany.</strong><strong>selenium.</strong><strong>lib.</strong><strong>pages</strong>” и добавим в него 3 класса страниц, с которыми работает наш тест:</p>
<p><strong>MainPage</strong>:</p><p>
/**
*
*/
package com.mycompany.selenium.lib.pages;
import com.mycompany.selenium.lib.ExtendedSelenium;
import com.mycompany.selenium.lib.PageObjectClass;
/**
- @author KaNoN
*/
public class MainPage extends PageObjectClass {
/**
* @param selenium
* @throws Exception
*/
public MainPage(ExtendedSelenium selenium) throws Exception {
super(selenium);
}
public NewJobPage clickOnNewJobLink() throws Exception{
selenium.clickAndWait( "leftpanel.newjob" );
return new NewJobPage( selenium );
}
public MainPage clickOnHudsonLink() throws Exception {
selenium.clickAndWait("leftpanel.hudson");
return this;
}
}
</p>
<p><strong>NewJobPage</strong>:</p><p>
/**
*
*/
package com.mycompany.selenium.lib.pages;
import com.mycompany.selenium.lib.ExtendedSelenium;
import com.mycompany.selenium.lib.PageObjectClass;
/**
- @author KaNoN
*/
public class NewJobPage extends PageObjectClass {
/**
* @param selenium
* @throws Exception
*/
public NewJobPage(ExtendedSelenium selenium) throws Exception {
super(selenium);
}
public NewJobPage typeJobName( String name ) throws Exception {
selenium.type( "newjobpage.name", name );
return this;
}
public NewJobPage checkFreeStyleJobRadioButton() throws Exception {
selenium.click("newjobpage.freestyleradio");
return this;
}
public ConfigureJobPage clickOK() throws Exception{
selenium.clickAndWait("newjobpage.ok");
return new ConfigureJobPage( selenium );
}
}
</p>
<p><strong>ConfigureJobPage</strong>:</p><p>
/**
*
*/
package com.mycompany.selenium.lib.pages;
import com.mycompany.selenium.lib.ExtendedSelenium;
import com.mycompany.selenium.lib.PageObjectClass;
/**
- @author KaNoN
*/
public class ConfigureJobPage extends PageObjectClass {
/**
* @param selenium
* @throws Exception
*/
public ConfigureJobPage(ExtendedSelenium selenium) throws Exception {
super(selenium);
}
public MainPage clickSave() throws Exception {
selenium.clickAndWait("configurejob.save");
return new MainPage( selenium );
}
}
</p><p>Если посмотреть внимательно, то можно увидеть,
что каждое действие, которое возвращает объект страницы, либо создает новый
объект, либо возвращает указатель на себя, если перехода на новую страницу не
было.<!--break--></p><p>Теперь
надо полученное решение внедрить в наш тест, с учетом того, что подобных тестов
с теми же страницами может быть много. К тому же, неудобно каждый раз создавать
объекты страниц в каждом тесте. Нужно, чтобы это делалось где-то в одном месте.
А мы для этого предусмотрительно создали базовый класс для тестов. Подправим
его таким образом, чтоб он приобрел следующий вид:</p><p>
/**
*
*/
package com.mycompany.selenium.lib;
import com.mycompany.selenium.lib.pages.*;
import java.util.Hashtable;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
/**
- @author KaNoN
*/
public class BaseTestClass extends SecurityManager {
protected ExtendedSelenium selenium = null;
protected String delay = "";
protected MainPage mainPage = null;
protected NewJobPage newJobPage = null;
protected ConfigureJobPage configureJobPage = null;
@BeforeMethod(alwaysRun=true)
public void init() throws Exception {
delay = Config.getProperty( "delay" );
selenium = new ExtendedSelenium( Config.getProperty( "host" ) , new Integer( Config.getProperty( "port" ) ) , Config.getProperty( "browser" ) , Config.getProperty( "url" ) );
selenium.start();
selenium.open( Config.getProperty( "url" ) + "/hudson" );
mainPage = new MainPage( selenium );
}
@AfterMethod(lastTimeOnly=true)
public void stop() throws Exception {
selenium.stop();
}
}
</p><p>Что здесь поменялось? Мы в базовый класс
добавили объекты страниц, которые мы создали ранее и в методе <strong>init</strong>
явно проинициализировали объект главной страницы <strong>mainPage</strong>. После
этих правок, мы можем исправить наш тест и привести вот к такому виду:</p><p>
package com.mycompany.selenium.tests;
import com.mycompany.selenium.lib.BaseTestClass;
import java.util.Date;
import org.testng.annotations.Test;
/**
- @author KaNoN
*/
public class SampleTestStage04 extends BaseTestClass {
private String genTaskName() {
Date dt = new Date();
String result = dt.toString();
result = result.replaceAll( ":" , "" );
result = result.replaceAll( " " , "" );
return result;
}
@Test(groups = {"sample","sample4"})
public void testCreateJob() throws Exception {
newJobPage = mainPage.clickOnNewJobLink();
newJobPage.typeJobName( "SampleTask" + genTaskName() );
newJobPage.checkFreeStyleJobRadioButton();
configureJobPage = newJobPage.clickOK();
mainPage = configureJobPage.clickSave();
mainPage.clickOnHudsonLink();
}
}
</p><p>Уже лучше. Теперь наши операции в тестах описываются на уровне выполняемых действий с учетом логики приложения. Теперь мы видим, с какими страницами мы работаем и нам не надо будет править локаторы или их псевдонимыв разных местах теста. Нам нужно будет просто внести корректировки в соответствующий класс страницы.</p>
<p>При этом возникает вопрос, а зачем теперь карта локаторов? На самом деле это только в примере для каждого элемента был создан только один метод в соответствующем классе страницы. Но на практике, один и тот же элемент может использовать разные действия. Например, для чек-бокса надо и установить/снять отметку, проверить его состояние, иногда просто проверить существование элемента. Всё это на уровне класса страницы выражается минимум 4-мя методами. То есть один и тот же локатор используется в 4-х местах. В этом случае карта объектов позволяет дополнительно оптимизировать затраты на корректировку/поддержку общего решения.</p>
<h2>Шаг 5: вынесение кода на уровень бизнес-функционала</h2>
<p>Вышеперечисленные
улучшения заметно упрощают читаемость кода тестов. Но во-первых, хоть мы можем
проследить последовательность действий, логический смысл этих действий еще не
так уж и ясен. То есть, какую же бизнес-операцию мы хотим сделать? Во-вторых,
если подобная последовательность действий встречается в нескольких местах, то в
случае изменения workflow, набор операций надо будет переделать и внести изменения во всех
местах, где используется данный workflow.<br>Таким образом, для
лучшей переиспользуемости кода нам нужно перейти с уровня последовательности
операций на уровень последовательности бизнес-функций. </p><p>Теперь посмотрим на
тестовый код. Что он по сути делает? Он создает новую задачу в Hudson, при этом никаких
дополнительных настроек не выполняется. То есть, нам надо создать пустую
задачу. И нам нужно, чтобы тест оперировал примерно такими же терминами, чтобы
было видно, что он не просто делает какие-то действия со страницами, а
выполняет некоторую четко определенную операцию.</p>
<p>Реализуем эту
операцию в виде статического метода. Нам не нужно делать экземпляр класса для
выполнения общих операций, не привязанных к конкретным объектам. Создадим пакет
“com.mycompany.selenium.lib.actions” и добавим в него
класс HudsonJobs со следующим содержимым:</p><p>
/**
*
*/
package com.mycompany.selenium.lib.actions;
import com.mycompany.selenium.lib.PageFactory;
import com.mycompany.selenium.lib.pages.*;
/**
- @author KaNoN
*/
public class HudsonJobs {
public static void createEmpty( String taskName ) throws Exception {
// TODO Add code here
}
}
</p>
<p>Мы пока создали скелет, некий прототип того,
что должно быть. И все потому, что вот просто так вставить вот такой код:</p><p>
newJobPage = mainPage.clickOnNewJobLink();
newJobPage.typeJobName( "SampleTask" + genTaskName() );
newJobPage.checkFreeStyleJobRadioButton();
configureJobPage = newJobPage.clickOK();
mainPage = configureJobPage.clickSave();
mainPage.clickOnHudsonLink();
</p>
<p>не получится. Вернее, получится (как-нибудь),
но работать как надо он не будет. Всё упирается в объект страницы, хранящийся в
переменной <strong>mainPage</strong>. Его надо
проинициализировать и передать объект Selenium-а. Причем сделать это желательно неявно.
На этом уровне явные обращения к Selenium-у уже должны быть исключены, так как для этого у нас есть уровень
объектов страниц.</p>
<p>Передавать
объект параметром тоже неудобно, так как нам надо будет тогда в тестах
передавать его во всех вхождениях подобных методов, что слабо вяжется с
назначением подобных методов.</p>
<p>Одним из достаточно
оптимальных решений будет использование фабрики объектов страниц. То есть, будет
некоторый класс, который создаст объект страницы, а в качестве параметра
инициализации (объект Selenium-a) передаст
объект Selenium-a,
который используется сейчас (напомню, что перед началом каждого теста он
создается). А для этого нам надо как-то еще получить тестовый класс, который
вызывается в данный момент. То есть, нам еще нужно сделать привязку тестового
класса к объекту Selenium-a.</p>
<p>Итак, делаем
улучшения в обратном порядке. Вначале добавим в BaseTestClass вот такое поле:</p><p>
public static Hashtable<String,ExtendedSelenium> sessionTable = new Hashtable<String,ExtendedSelenium>();
</p>
<p>В этой таблице мы будем хранить пары «имя
класса – объект Selenium-a». После
этого нам нужно получить имя класса, который фактически сейчас работает (мы
ведь оперируем с базовым классом, а нужен его наследник). Для этого в BaseTestClass добавим
метод:</p><p>
private String getTestClass() throws Exception {
Class<?> classes[] = this.getClassContext();
for( Class<?> clazz:classes ){
if( clazz.getCanonicalName().startsWith( “com.mycompany.selenium.tests” ) ){
return clazz.getName();
}
}
return “”;
}
</p>
<p>И где-то при инициализации вставим строку
вида:</p><p>
BaseTestClass.sessionTable.put( getTestClass() , selenium );
</p>
<p>Всё, вот теперь данный класс содержит в себе
таблицу используемых тестовых классов и объектов Selenium-a. В итоге, данный класс имеет вид:</p><p>
/**
*
*/
package com.mycompany.selenium.lib;
import com.mycompany.selenium.lib.pages.*;
import java.util.Hashtable;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
/**
- @author KaNoN
*/
public class BaseTestClass extends SecurityManager {
protected ExtendedSelenium selenium = null;
protected String delay = "";
public static Hashtable<String,ExtendedSelenium> sessionTable = new Hashtable<String,ExtendedSelenium>();
protected MainPage mainPage = null;
protected NewJobPage newJobPage = null;
protected ConfigureJobPage configureJobPage = null;
private String getTestClass() throws Exception {
Class<?> classes[] = this.getClassContext();
for( Class<?> clazz:classes ){
if( clazz.getCanonicalName().startsWith( "com.mycompany.selenium.tests" ) ){
return clazz.getName();
}
}
return "";
}
public ExtendedSelenium getSelenium(){
return selenium;
}
@BeforeMethod(alwaysRun=true)
public void init() throws Exception {
delay = Config.getProperty( "delay" );
selenium = new ExtendedSelenium( Config.getProperty( "host" ) , new Integer( Config.getProperty( "port" ) ) , Config.getProperty( "browser" ) , Config.getProperty( "url" ) );
selenium.start();
selenium.open( Config.getProperty( "url" ) + "/hudson" );
BaseTestClass.sessionTable.put( getTestClass() , selenium );
mainPage = new MainPage( selenium );
}
@AfterMethod(lastTimeOnly=true)
public void stop() throws Exception {
selenium.stop();
}
}
</p><p>Единственное, тут следует отметить, что тесты
ищутся в пределах пакета «com.mycompany.selenium.tests». То есть изначально в дизайн закладывается, что сами тестовые классы
будут находиться именно внутри этого пакета и вложенных пакетов. </p>
<p>Теперь перейдем к
созданию фабрики страниц. Это некоторый класс, который содержит метод для
создания страницы и возврата созданного объекта. И должен быть вспомогательный
метод к нему, который определит, какой же тестовый класс сейчас работает. В
пакете «<strong>com.mycompany.selenium.lib</strong>»
создадим класс PageFactory, содержимое которого выглядит примерно так:</p><p>
/**
*
*/
package com.mycompany.selenium.lib;
/**
- @author KaNoN
*/
public class PageFactory extends SecurityManager {
private String getTestClass() throws Exception {
Class<?> classes[] = this.getClassContext();
for( Class<?> clazz:classes ){
if( clazz.getCanonicalName().startsWith( "com.mycompany.selenium.tests" ) ){
return clazz.getName();
}
}
return "";
}
public static Object getPage( Class<?> pageClass ) throws Exception {
PageFactory factory = new PageFactory();
ExtendedSelenium selenium = BaseTestClass.sessionTable.get( factory.getTestClass() );
return pageClass.getConstructor( ExtendedSelenium.class ).newInstance( selenium );
}
}
</p>
<p>После этого мы, зная класс нужного нам объекта
страницы, можем его создать. Ключевой метод <strong>getPage</strong> как
раз принимает парамметром класс страницы и вызывает его конструктор с
параметром Selenium-объектом. А последний получен из <strong>BaseTestClass</strong>, который после последних изменений содержит таблицу пар «имя класса –
объект Selenium-a». </p>
<p>Теперь вернемся
к классу <strong>HudsonJobs</strong> и его статическому методу <strong>createEmpty</strong>, который мы оставили пока пустым, но для него и делались все
последующие ухищрения. Итак, в коде, который планировался быть помещенным в
этот метод, проблемы вызывала только первая строка, в которой нам надо было
получить объект класса <strong>MainPage</strong>. Но сейчас мы
это можем сделать, используя PageFactory.getPage( <Page class> ). В результате,
данный класс приобретает вид:</p><p>
/**
*
*/
package com.mycompany.selenium.lib.actions;
import com.mycompany.selenium.lib.PageFactory;
import com.mycompany.selenium.lib.pages.*;
/**
- @author KaNoN
*/
public class HudsonJobs {
public static void createEmpty( String taskName ) throws Exception {
MainPage mainPage = (MainPage) PageFactory.getPage( MainPage.class );
NewJobPage newJobPage = mainPage.clickOnNewJobLink();
newJobPage.typeJobName( taskName );
newJobPage.checkFreeStyleJobRadioButton();
ConfigureJobPage configureJobPage = newJobPage.clickOK();
mainPage = configureJobPage.clickSave();
mainPage.clickOnHudsonLink();
}
}
</p>
<p>По сути надо было добавить строку, которая
подсвечена желтым в примере выше.</p>
<p>И последний штрих –
применим данный класс к нашему тесту. Фактически, весь наш тест полностью
покрывается одним методом, поэтому тест сужается практически до минимальных
размеров и принимает вид:</p><p>
/**
*
*/
package com.mycompany.selenium.tests;
import com.mycompany.selenium.lib.BaseTestClass;
import com.mycompany.selenium.lib.actions.HudsonJobs;
import java.util.Date;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
/**
- @author KaNoN
*/
public class SampleTestStage05 extends BaseTestClass {
private String genTaskName() {
Date dt = new Date();
String result = dt.toString();
result = result.replaceAll( ":" , "" );
result = result.replaceAll( " " , "" );
return result;
}
@BeforeMethod(alwaysRun=true)
public void init() throws Exception {
super.init();
}
@Test(groups = {"sample","sample5"})
public void testCreateJob() throws Exception {
HudsonJobs.createEmpty( "SampleTask" + genTaskName() );
}
}
</p>
<p>Всё,теперь структуру тестов оптимизировать уже особо некуда. Тесты уже оперируют бизнес-функциями,
а не отдельной последовательностью действий. Все ключевые действия вынесены на соответствующий уровень абстракции, варьируемые параметры вынесены в конфигурационные файлы и повторяемость кода сведена к минимуму. Теперь это решение можно расширять, дорабатывать и поддерживать. Даже если функционал поменялся, нам теперь придется переделывать не столько тесты, сколько бизнес-функции, локаторы.</p>
<p>Что дальше? На уровне кода уже особо улучшать нечего. Дальше это решение уже можно
использовать. Также, поскольку мы уже оперируем бизнес-функциями, которые фактически являются командами, то можно усовершенствовать набор таких функций и поставить им в соответствие некоторые ключевые слова. Там мы плавно переходим к Keyword-driven подходу. Если надо, конечно же. Мы можем работать над другими направлениями, как постановка интегрированной инфраструктуры, репортинг, вспомогательный функционал и многое другое.</p><p></p><h2 style="text-align: right; "><a href="http://automated-testing.info/t/selenium-rc-java-shagi-usovershenstvovaniya-testov-chast-1/2257" target="_blank" title="Selenium RC (Java): Шаги усовершенствования тестов. Часть 1">Часть 1</a></h2><p></p>