Selenium RC (Java): Шаги усовершенствования тестов. Часть 2

Часть 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>После этого, мы можем создавать классы
страниц, которые (классы) будут отнаследованы от данного класса.&nbsp;Еще
один момент. Когда мы работаем с объектом страницы, то в ряде случаев, когда мы
делаем действие, приводящее к переходу на новую страницу, было бы полезно
возвращать объект этой новой страницы.&nbsp;Учитывая
эти пожелания, создадим отдельный пакет для классов страниц. Назовем его “<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>.&nbsp;После
этих правок, мы можем исправить наш тест и привести вот к такому виду:</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>Таким образом, для
лучшей переиспользуемости кода нам нужно перейти с уровня последовательности
операций на уровень последовательности бизнес-функций.&nbsp;</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>

А также хочется добавить, что данный подход дает возможность обратного (реверсного) построения тестов.
Т.е. сначала делается фреймворк, наподобие как представил KaNoN, а дальше делаются тесты по принципу, TDD (Test Driven Development).
А именно проектируются тесты на основании бизнес логики на самом последнем уровне абстракции, тест конечно же не работает и красный.
Потом, добавляем все необходимое в методы и классы, для того что бы данный тест "озеленился".
Данный подход дает возможность понимания, какие классы и методы необходимы, а также какие данные будут туда передаваться.