Под статическим ресурсами понимаются те виды ресурсов, которые не изменяются в процессе тестирования или работы с приложением. К ним можно отнести названия и атрибуты элементов страниц, текст внутри элементов на странице, статусы документов и т.д.
Важной задачей при написании автоматических тестов и фреймворков является организация работы со статическими ресурсами, а именно: способ доступа и чтения необходимых данных.
Существует 2 основных варианта организации работы со статическими данными, которые имеют свои преимущества и недостатки:
- Прописывание данных внутри кода - hardcode (см. использование констант в Java)
Недостатки:
- при изменении данных необходимо будет по-новой пере-собирать тесты
все статические ресурсы загружаются и хранятся в памяти и не могут быть освобождены.
- доступ к данным максимально упрощен
- Вынесение статических значений из кода (в БД, в файлы и т.д.)
Недостатки:
- операция чтения из БД или файлов занимает определенное время и может значительно замедлить выполнение тестов.
файл или таблица БД может "залочиться", в случае одновременного использования одних и тех же ресурсов- возможность структурирования статических ресурсов в БД или файловой системе
- ресурсы из файла могу быть прочитаны, использованы тестом, удалены из памяти, а затем при необходимости снова прочитаны
возможность изменения статических ресурсов без последующего изменения в коде и пере-собирания тестов
По правде говоря, я не являюсь большим поклонником жесткого прописывания статических ресурсов в коде, хотя сторонников этого подхода хватает и у них есть веские причины следовать ему (неудобства и ограничения внутри 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
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.javapublic 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.javapublic 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.javapublic 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, при запросе названия свойства, выбор идет из списка имеющихся, т.е. наизусть запоминать не надо :)
Отправить комментарий