пятница, 23 июля 2010 г.

Автоматизированное тестирование: работа со статическими ресурсами

Под статическим ресурсами понимаются те виды ресурсов, которые не изменяются в процессе тестирования или работы с приложением. К ним можно отнести названия и атрибуты элементов страниц, текст внутри элементов на странице, статусы документов и т.д.

Важной задачей при написании автоматических тестов и фреймворков является организация работы со статическими ресурсами, а именно: способ доступа и чтения необходимых данных.

Существует 2 основных варианта организации работы со статическими данными, которые имеют свои преимущества и недостатки:

  1. Прописывание данных внутри кода - hardcode (см. использование констант в Java)
    Недостатки:
    • при изменении данных необходимо будет по-новой пере-собирать тесты
    • все статические ресурсы загружаются и хранятся в памяти и не могут быть освобождены.
    Преимущества:
    • доступ к данным максимально упрощен
  2. Вынесение статических значений из кода (в БД, в файлы и т.д.)
    Недостатки:
    • операция чтения из БД или файлов занимает определенное время и может значительно замедлить выполнение тестов.
    • файл или таблица БД может "залочиться", в случае одновременного использования одних и тех же ресурсов
    Преимущества:
    • возможность структурирования статических ресурсов в БД или файловой системе
    • возможность изменения статических ресурсов без последующего изменения в коде и пере-собирания тестов
    • ресурсы из файла могу быть прочитаны, использованы тестом, удалены из памяти, а затем при необходимости снова прочитаны




По правде говоря, я не являюсь большим поклонником жесткого прописывания статических ресурсов в коде, хотя сторонников этого подхода хватает и у них есть веские причины следовать ему (неудобства и ограничения внутри IDE при работе с именами параметров, необходимость быстрого и максимально упрощенного доступа к данным).

Имея достаточное количество наработок при использовании файловой системы, в качестве контейнера для хранения статических ресурсов, я хотел бы поделиться своим методом работы с ними на базе Java Properties файлов.

Несколько слов о том, что такое проперти файл (Properties).

Это обычный текстовый файл, данные в котором хранятся в виде key = value Пример: LoginPage.properties

----------------
field.username.id = UserName
field.password.id = Password
button.login.id = LoginButton
----------------

Пример Java кода, который читает LoginPage.properties файл и получает необходимую информацию:

----------------
// инициализация LoginPage.properties
Properties properties = new Properties();
File propertyFile = new File(LoginPage.properties);
properties.load(new FileReader(propertyFile));
// чтение данных
String userNameID = properties.getProperty("field.username.id");
String passwordID = properties.getProperty("field.password.id");
String loginButtonID = properties.getProperty("button.login.id");
----------------

Конечно избавиться от хардкода нам не удастся на все 100%, однако в последнем случае жестко прописаны ключи к для доступа к данным, а не сами данные. Что является уже не хардкодом, а скажем "параметризацией".

Теперь на базе этого я расскажу, как построен тестовый фреймворк основанный на примере описанном в предыдущей статье PageObjects pattern + Selenium (Java)

Основными принципами работы со статическими ресурсами в нашем приложении является то, то для каждого объекта страницы создается отдельный проперти файл с идентичным именем, хранящимися в проектном каталоге resources. Таким образом нам необходимо создать 3 файла и заполнить в них статическую информацию:

LoginPage.properties
----------------
field.username.locator = id
field.username.arg = UserName
field.password.locator = id
field.password.arg = Password
button.login.locator = id
button.login.arg = LoginButton
----------------
HomePage.properties
----------------
text.username.locator = id
text.username.arg = userName
link.logout.locator = id
link.logout.arg = LogoutLink
----------------
ErrorLoginPage.properties
----------------
text.errormessage.locator = id
text.errormessage.arg = ErrorMessage
link.backtologin.locator = id
link.backtologin.arg = BackLink
----------------

Создав все необходимые ресурсы, перейдем к написанию Java кода, который будет осуществлять загрузку, хранение и чтение данных. Для хранения загруженных ресурсов создадим класс DataStorage. В нем будет храниться Map данных (Map propertiesMap), ключом в котором будет являться название объекта страницы, а значением объект Properties со списком статических данных.

DataStorage.java
public class DataStorage {
   private Map propertiesMap;

   private DataStorage() {
       this.propertiesMap = new HashMap();
   }

   public static DataStorage getInstance() {
       return new DataStorage();
   }

   public void setProperty(String key, Properties properties) {
       propertiesMap.put(key, properties);
   }

   public Properties getProperty(String key) {
       return propertiesMap.get(key);
   }

   public boolean exists(String key) {
       return propertiesMap.get(key) != null;
   }
}

Добавим ссылку на него в рабочем контексте, а также несколько сервисных методов для работы с ним (выделено жирным в листинге класса):

Context.java
public class Context {
   public static final String BROWSER_IE = "*iexplore";
   public static final String BROWSER_FF = "*firefox";
   public static final String BROWSER_CH = "*chrome";
   private static final String RESOURCES_PATH = "resources/${NAME}.properties";    

   public static String siteUrl;
   private static Context context;
   private DataStorage dataStorage;
   private Selenium selenium;
   private SeleniumServer seleniumServer;

   private Context() {
       this.setDataStorage(DataStorage.getInstance());
   }

   public static void initInstance(String browserType, String siteURL) {
       context = new Context();
       siteUrl = siteURL;
       context.setSelenium(new DefaultSelenium("localhost", 4444, browserType, siteURL));
       context.start();
   }

   public static Context getInstance() {
       if (context == null) {
           throw new IllegalStateException("Context is not initialized");
       }
       return context;
   }

   public Selenium getSelenium() {
       if (selenium != null) {
           return selenium;
       }
       throw new IllegalStateException("WebBrowser is not initialized");
   }

   public void start() {
       try {
           seleniumServer = new SeleniumServer();
           seleniumServer.start();
       } catch (Exception e) {
           e.printStackTrace();
       }
       selenium.start();
   }

   public void close() {
       selenium.close();
       selenium.stop();
       seleniumServer.stop();
   }

   public String getSiteUrl() {
       return siteUrl;
   }

   public void setSelenium(Selenium selenium) {
       this.selenium = selenium;
   }

   public String getResourcesPath(String name) {
       return RESOURCES_PATH.replaceAll("\\$\\{NAME\\}", name);
   }

   private void setDataStorage(DataStorage dataStorage) {
       this.dataStorage = dataStorage;
   }

   public DataStorage getDataStorage() {
       return dataStorage;
   }
}

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

Page.java
public abstract class Page {
   private Context context;
   private String currentPage;
   private Properties properties;
   
   protected Page(String pageUrl) {
       this.currentPage = pageUrl;
       setContext(Context.getInstance());
       initProperties();
       init();
       parsePage();
   }

   private void initProperties() {
       String className = getClass().getSimpleName();
       if (!getContext().getDataStorage().exists(className)) {
           this.properties = new Properties();
           List superClasses = ClassUtils.getAllSuperclasses(getClass());
           File file = null;
           for (int i = superClasses.size() - 2; i >= 0 ; i--) {
               Class aClass = (Class) superClasses.get(i);
               file = new File(getResourcesPath(aClass.getSimpleName()));
               if (getContext().getDataStorage().getProperty(aClass.getSimpleName())== null) {
                   if (file.exists()) {
                       putAllProperties(file);
                       updateStogare(aClass.getSimpleName(), getProperties());
                   }
               } else {
                   putAllProperties(getContext().getDataStorage().getProperty(aClass.getSimpleName()));
               }
           }
           file = new File(getResourcesPath(className));
           putAllProperties(file);
           updateStogare(this, getProperties());
       } else {
           setProperties(getContext().getDataStorage().getProperty(getClass().getSimpleName()));
       }
   }
   
   protected abstract void init();
   protected abstract void parsePage();

   private void setContext(Context instance) {
       this.context = instance;
   }

   public Context getContext() {
       return context;
   }

   public String getCurrentPage() {
       return context.getSiteUrl() + this.currentPage;
   }

   protected Selenium getSelenium() {
       return context.getSelenium();
   }

   private String getResourcesPath(String name) {
       return getContext().getResourcesPath(name);
   }
   
   private Properties getProperties() {
       return properties;
   }
   
   private void setProperties(Properties properties) {
       this.properties = properties;
   }
   
   protected String getProperty(String key) {
       return properties.getProperty(key);
   }
   
   private void putAllProperties(File proertiesFile) {
       try {
           this.properties.load(new FileReader(proertiesFile));
       } catch (IOException e) {
           e.printStackTrace();
       }
   }
   
   private void putAllProperties(Properties properties) {
       this.properties.putAll(properties);
   }

   private void updateStogare(Object parentKeyObj, Properties properties) {
       updateStogare(parentKeyObj.getClass().getSimpleName(), properties);
   }

   private void updateStogare(String className, Properties properties) {
       getContext().getDataStorage().setProperty(className, (Properties)properties.clone());
   }

   protected String buildLocator(String type, String arg) {
       // Сервисный метода для создания Selenium локатора 
       // по двум параметрам тип и аргумент
       return type + "=" + arg;
   }

   // ....
   // service methods...
   // ....
}

Заменив в объектах станиц, конкретные значения статических ресурсов на чтение их значений из объектов Properties мы получаем их следующую реализацию:

LoginPage.java
public class LoginPage extends Page {
   public static final String PAGE_URL = "http://www.testlogin.com/login.html";

    protected LoginPage() {
        super(PAGE_URL);
    }

    public static LoginPage openLoginPage() {
        LoginPage loginPage = new LoginPage();
        loginPage.getSelenium().open(PAGE_URL);
        return loginPage;
    }

    private void setUserName(String userName) {
        // код для заполнения поля Username
        getSelenium().type(buildLocator(getProperty("field.username.locator"),getProperty("field.username.arg")), userName);
   }

    private void setPassword(String password) {
        // код для заполнения поля Password
        getSelenium().type(buildLocator(getProperty("field.password.locator"), getProperty("field.password.arg")), password);
   }

    private void pushLoginButton() {
        // код для нажатия на кнопку Login
        getSelenium().click(buildLocator(getProperty("button.login.locator"), getProperty("button.login.arg")));
   }

    protected void parsePage() {
        // Разбор элементов страницы
        // Заполнение необходимых переменных данными со страницы
    }

    protected void init() {
        // Инициализация страницы
        // Проверка корректности загрузки
        if(!getSelenium().getLocation().equals(PAGE_URL)) {
            throw new IllegalStateException("Invalid page is opened");
        }
    }

    private void loginAs(String userName, String password) {
        setUserName(userName);
        setPassword(password);
        pushLoginButton();
    }

    public HomePage login(String userName, String password) {
        loginAs(userName, password);
        return new HomePage();
    }

    public ErrorLoginPage loginInvalid(String userName, String password) {
        loginAs(userName, password);
        return new ErrorLoginPage();
    }
}
HomePage.java
public class HomePage extends Page {
    public static final String PAGE_URL = "http://www.testlogin.com/home.html";
    private String loggedinUserName;

    protected HomePage() {
        super(PAGE_URL);
    }

    protected void init() {
// Инициализация страницы
    }

    protected void parsePage() {
// Разбор элементов страницы
        this.loggedinUserName = getSelenium().getText(buildLocator(getProperty("text.username.locator"), getProperty("text.username.id")));
   }

    public String getLoggedinUserName() {
        return loggedinUserName;
    }

    public LoginPage logout() {
        getSelenium().click(buildLocator(getProperty("link.logout.locator"), getProperty("link.logout.id")));
       return new LoginPage();
    }
}
ErrorLoginPage.java
public class ErrorLoginPage extends Page {
    public static final String PAGE_URL = "http://www.testlogin.com/loginError.html";
    private String errorMessage;

    protected ErrorLoginPage() {
        super(PAGE_URL);
    }

    protected void init() {
// Инициализация страницы
    }

    protected void parsePage() {
        this.errorMessage = getSelenium().getText(buildLocator(getProperty("text.errormessage.locator"), getProperty("text.errormessage.id")));
   }

    public String getErrorMessage() {
        return this.errorMessage;
    }

    public LoginPage backToLoginPage() {
        getSelenium().click(buildLocator(getProperty("link.backtologin.locator"), getProperty("link.backtologin.id")));
       return new LoginPage();
    }
}

Скачать исходный код примеров по работе со статическими ресурсами

Обратите внимание, что в теперешней реализации мы вынесли все статические ресурсы в файлы. И теперь в случае, если какие-то данные будут изменены, например, id “UserName” изменится на “user_name” и нужно будет изменить тип локатора для поиска c “id” на “xpath”, то нам всего навсего надо будет заменить значение в файле, оставив код фреймворка без изменения:

field.username.locator = xpath
field.username.arg = //input[@id=’user_name’]

Описанный выше подход при работе со статическими ресурсами, используется уже на протяжении двух лет, и значит он в полевых условиях доказал свое право на существование. Надеюсь, что после внимательного прочтения кому-то тоже захочется реализовать у себя что-то похожее, либо абсолютно такое же.

ЗЫ Комментарии приветствуются, критика тоже... жду отзывов...

Да приумножится Ваша Автоматизация, Алексей Булат

2 комментария:

Анонимный комментирует...

Мы на .NET используем resource-файлы (*.resx). Нету такого же на java?

Использовать очень удобно, что-то вида:
Button.Click(HomePageMap.LoginXPath);

А.Б. комментирует...

В Java для этих целей именно Properties и используют. В более менее дружелюбных IDE, при запросе названия свойства, выбор идет из списка имеющихся, т.е. наизусть запоминать не надо :)

Условия копирования публикаций:

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