Selenium RC (Java): Шаги усовершенствования тестов. Часть 2
Опубликовано KaNoN в 16.07.2010
Часть 1
В предыдуших шагах мы позаботились, пожалуй о самой чувствительной к изменениям части - идентификаторах объектов. Но этого еще недостаточно, есть еще несколько шагов, которые нужно сделать, чтобы минимизировать затраты на поддержку, а также время на разработку, да и просто сделать тесты более понятными и удобными для чтения. Итак, рассмотрим эти шаги.
Шаг 4: Абстрагируемся до уровня действий на странице
В предыдущем шаге мы сумели вынести локаторы в отдельный файл и, задав, понятные имена, мы можем уже проследить, над какими элементами проводятся операции. Тем не менее, мы по-прежнему работаем на уровне примитивных команд, которые мало к чему привязаны. К тому же слабо прослеживается переход с одной страницы на другую.
Другой момент заключается в том, что держать в голове все псевдонимы всех элементов неудобно, особенно для больших объемов тестов. То есть надо бы как-то сделать так, чтобы и псевдонимы элементов использовались не так интенсивно. Да и хотелось бы, чтобы тестовые инструкции выглядели более информативно, например, не
selenium.clickAndWait( "leftpanel.newjob" );
а что-то наподобие
mainPage.clickOnNewJobLink();
То есть примитивные операции обернуть в некоторый функционал, который уже отражал бы смысл операции. Это так называемый PageObject-подход, при котором некоторому отдельному окну/странице/форме соответствует некоторый класс, методы которого соответствуют либо каким-то дочерним элементам, либо примитивным действиям внутри данного окна/страницы/формы. Один из примеров подобной реализации можно описан здесь: http://autotestgroup.com/ru/blog/55.html, а точнее реализация подобного для TestComplete. Там было описано, как обернуть некоторые дочерние элементы. В нашем случае применим подход обертки действий над некоторыми элементами, так как Selenium больше ориентирован на действия, которые проводятся над объектом, а не на объекты, над которыми проводятся действия. Это достаточно тонкая грань, которую надо уметь усмотреть.
В любом случае, нам нужен некоторый набор классов, которые могли бы соответствовать некоторым страницам. Сразу следует обратить внимание на то, что если мы скрываем действия Selenium-а внутри некоторого внешнего класса, то нам надо в этот класс передать объект Selenium-а, созданный тестом. Например, при создании любого объекта страницы в качестве параметра передается объект Selenium-a. Пожалуй, это будет наиболее общее для всех объектов страниц решение. В пакет com.mycompany.selenium.lib добавим класс BaseTestClass со следующим содержимым:
/**
*
*/
package com.mycompany.selenium.lib;
/**
* @author KaNoN
*
*/
public class PageObjectClass {
protected ExtendedSelenium selenium = null;
public PageObjectClass( ExtendedSelenium selenium ) throws Exception {
this.selenium = selenium;
}
}
После этого, мы можем создавать классы страниц, которые (классы) будут отнаследованы от данного класса. Еще один момент. Когда мы работаем с объектом страницы, то в ряде случаев, когда мы делаем действие, приводящее к переходу на новую страницу, было бы полезно возвращать объект этой новой страницы. Учитывая эти пожелания, создадим отдельный пакет для классов страниц. Назовем его “com.mycompany.selenium.lib.pages” и добавим в него 3 класса страниц, с которыми работает наш тест:
MainPage:
/**
*
*/
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;
}
}
NewJobPage:
/**
*
*/
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 );
}
}
ConfigureJobPage:
/**
*
*/
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 );
}
}
Если посмотреть внимательно, то можно увидеть, что каждое действие, которое возвращает объект страницы, либо создает новый объект, либо возвращает указатель на себя, если перехода на новую страницу не было.
Теперь надо полученное решение внедрить в наш тест, с учетом того, что подобных тестов с теми же страницами может быть много. К тому же, неудобно каждый раз создавать объекты страниц в каждом тесте. Нужно, чтобы это делалось где-то в одном месте. А мы для этого предусмотрительно создали базовый класс для тестов. Подправим его таким образом, чтоб он приобрел следующий вид:
/**
*
*/
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();
}
}
Что здесь поменялось? Мы в базовый класс добавили объекты страниц, которые мы создали ранее и в методе init явно проинициализировали объект главной страницы mainPage. После этих правок, мы можем исправить наш тест и привести вот к такому виду:
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();
}
}
Уже лучше. Теперь наши операции в тестах описываются на уровне выполняемых действий с учетом логики приложения. Теперь мы видим, с какими страницами мы работаем и нам не надо будет править локаторы или их псевдонимыв разных местах теста. Нам нужно будет просто внести корректировки в соответствующий класс страницы.
При этом возникает вопрос, а зачем теперь карта локаторов? На самом деле это только в примере для каждого элемента был создан только один метод в соответствующем классе страницы. Но на практике, один и тот же элемент может использовать разные действия. Например, для чек-бокса надо и установить/снять отметку, проверить его состояние, иногда просто проверить существование элемента. Всё это на уровне класса страницы выражается минимум 4-мя методами. То есть один и тот же локатор используется в 4-х местах. В этом случае карта объектов позволяет дополнительно оптимизировать затраты на корректировку/поддержку общего решения.
Шаг 5: вынесение кода на уровень бизнес-функционала
Вышеперечисленные
улучшения заметно упрощают читаемость кода тестов. Но во-первых, хоть мы можем
проследить последовательность действий, логический смысл этих действий еще не
так уж и ясен. То есть, какую же бизнес-операцию мы хотим сделать? Во-вторых,
если подобная последовательность действий встречается в нескольких местах, то в
случае изменения workflow, набор операций надо будет переделать и внести изменения во всех
местах, где используется данный workflow.
Таким образом, для
лучшей переиспользуемости кода нам нужно перейти с уровня последовательности
операций на уровень последовательности бизнес-функций.
Теперь посмотрим на тестовый код. Что он по сути делает? Он создает новую задачу в Hudson, при этом никаких дополнительных настроек не выполняется. То есть, нам надо создать пустую задачу. И нам нужно, чтобы тест оперировал примерно такими же терминами, чтобы было видно, что он не просто делает какие-то действия со страницами, а выполняет некоторую четко определенную операцию.
Реализуем эту операцию в виде статического метода. Нам не нужно делать экземпляр класса для выполнения общих операций, не привязанных к конкретным объектам. Создадим пакет “com.mycompany.selenium.lib.actions” и добавим в него класс HudsonJobs со следующим содержимым:
/**
*
*/
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
}
}
Мы пока создали скелет, некий прототип того, что должно быть. И все потому, что вот просто так вставить вот такой код:
newJobPage = mainPage.clickOnNewJobLink();
newJobPage.typeJobName( "SampleTask" + genTaskName() );
newJobPage.checkFreeStyleJobRadioButton();
configureJobPage = newJobPage.clickOK();
mainPage = configureJobPage.clickSave();
mainPage.clickOnHudsonLink();
не получится. Вернее, получится (как-нибудь), но работать как надо он не будет. Всё упирается в объект страницы, хранящийся в переменной mainPage. Его надо проинициализировать и передать объект Selenium-а. Причем сделать это желательно неявно. На этом уровне явные обращения к Selenium-у уже должны быть исключены, так как для этого у нас есть уровень объектов страниц.
Передавать объект параметром тоже неудобно, так как нам надо будет тогда в тестах передавать его во всех вхождениях подобных методов, что слабо вяжется с назначением подобных методов.
Одним из достаточно оптимальных решений будет использование фабрики объектов страниц. То есть, будет некоторый класс, который создаст объект страницы, а в качестве параметра инициализации (объект Selenium-a) передаст объект Selenium-a, который используется сейчас (напомню, что перед началом каждого теста он создается). А для этого нам надо как-то еще получить тестовый класс, который вызывается в данный момент. То есть, нам еще нужно сделать привязку тестового класса к объекту Selenium-a.
Итак, делаем улучшения в обратном порядке. Вначале добавим в BaseTestClass вот такое поле:
public static Hashtable<String,ExtendedSelenium> sessionTable = new Hashtable<String,ExtendedSelenium>();
В этой таблице мы будем хранить пары «имя класса – объект Selenium-a». После этого нам нужно получить имя класса, который фактически сейчас работает (мы ведь оперируем с базовым классом, а нужен его наследник). Для этого в BaseTestClass добавим метод:
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 "";
}
И где-то при инициализации вставим строку вида:
BaseTestClass.sessionTable.put( getTestClass() , selenium );
Всё, вот теперь данный класс содержит в себе таблицу используемых тестовых классов и объектов Selenium-a. В итоге, данный класс имеет вид:
/**
*
*/
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();
}
}
Единственное, тут следует отметить, что тесты ищутся в пределах пакета «com.mycompany.selenium.tests». То есть изначально в дизайн закладывается, что сами тестовые классы будут находиться именно внутри этого пакета и вложенных пакетов.
Теперь перейдем к созданию фабрики страниц. Это некоторый класс, который содержит метод для создания страницы и возврата созданного объекта. И должен быть вспомогательный метод к нему, который определит, какой же тестовый класс сейчас работает. В пакете «com.mycompany.selenium.lib» создадим класс PageFactory, содержимое которого выглядит примерно так:
/**
*
*/
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 );
}
}
После этого мы, зная класс нужного нам объекта страницы, можем его создать. Ключевой метод getPage как раз принимает парамметром класс страницы и вызывает его конструктор с параметром Selenium-объектом. А последний получен из BaseTestClass, который после последних изменений содержит таблицу пар «имя класса – объект Selenium-a».
Теперь вернемся к классу HudsonJobs и его статическому методу createEmpty, который мы оставили пока пустым, но для него и делались все последующие ухищрения. Итак, в коде, который планировался быть помещенным в этот метод, проблемы вызывала только первая строка, в которой нам надо было получить объект класса MainPage. Но сейчас мы это можем сделать, используя PageFactory.getPage( <Page class> ). В результате, данный класс приобретает вид:
/**
*
*/
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();
}
}
По сути надо было добавить строку, которая подсвечена желтым в примере выше.
И последний штрих – применим данный класс к нашему тесту. Фактически, весь наш тест полностью покрывается одним методом, поэтому тест сужается практически до минимальных размеров и принимает вид:
/**
*
*/
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() );
}
}
Всё,теперь структуру тестов оптимизировать уже особо некуда. Тесты уже оперируют бизнес-функциями, а не отдельной последовательностью действий. Все ключевые действия вынесены на соответствующий уровень абстракции, варьируемые параметры вынесены в конфигурационные файлы и повторяемость кода сведена к минимуму. Теперь это решение можно расширять, дорабатывать и поддерживать. Даже если функционал поменялся, нам теперь придется переделывать не столько тесты, сколько бизнес-функции, локаторы.
Что дальше? На уровне кода уже особо улучшать нечего. Дальше это решение уже можно использовать. Также, поскольку мы уже оперируем бизнес-функциями, которые фактически являются командами, то можно усовершенствовать набор таких функций и поставить им в соответствие некоторые ключевые слова. Там мы плавно переходим к Keyword-driven подходу. Если надо, конечно же. Мы можем работать над другими направлениями, как постановка интегрированной инфраструктуры, репортинг, вспомогательный функционал и многое другое.
Часть 1
»
- Войдите или зарегистрируйтесь, чтобы получить возможность отправлять комментарии









Обратное построение тестов
А также хочется добавить, что данный подход дает возможность обратного (реверсного) построения тестов.
Т.е. сначала делается фреймворк, наподобие как представил KaNoN, а дальше делаются тесты по принципу, TDD (Test Driven Development).
А именно проектируются тесты на основании бизнес логики на самом последнем уровне абстракции, тест конечно же не работает и красный.
Потом, добавляем все необходимое в методы и классы, для того что бы данный тест "озеленился".
Данный подход дает возможность понимания, какие классы и методы необходимы, а также какие данные будут туда передаваться.
------------------------------------------------------
Жизнь прекрасна, когда ты искренне радуешься жизни. :)
Миша Поляруш
Автор www.automated-testing.info и www.poliarush.com