Раздел: Автоматизация > Практикум > Автоматизированное тестирование: работа со статическими ресурсами
Автоматизированное тестирование: работа со статическими ресурсами
Под статическим ресурсами понимаются те виды ресурсов, которые не изменяются в процессе тестирования или работы с приложением. К ним можно отнести названия и атрибуты элементов страниц, текст внутри элементов на странице, статусы документов и т.д.
Важной задачей при написании автоматических тестов и фреймворков является организация работы со статическими ресурсами, а именно: способ доступа и чтения необходимых данных.
Существует 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.propertiesfield.username.locator = id field.username.arg = UserName field.password.locator = id field.password.arg = Password button.login.locator = id button.login.arg = LoginButtonHomePage.properties
text.username.locator = id text.username.arg = userName link.logout.locator = id link.logout.arg = LogoutLinkErrorLoginPage.properties
text.errormessage.locator = id text.errormessage.arg = ErrorMessage link.backtologin.locator = id link.backtologin.arg = BackLink
Создав все необходимые ресурсы, перейдем к написанию Java кода, который будет осуществлять загрузку, хранение и чтение данных. Для хранения загруженных ресурсов создадим класс DataStorage. В нем будет храниться Map данных (Map<String, Properties> propertiesMap), ключом в котором будет являться название объекта страницы, а значением объект Properties со списком статических данных.
DataStorage.javapublic class DataStorage { private Map<String, Properties> propertiesMap; private DataStorage() { this.propertiesMap = new HashMap<String, Properties>(); } 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); updateStorage(aClass.getSimpleName(), getProperties()); } } else { putAllProperties(getContext().getDataStorage().getProperty(aClass.getSimpleName())); } } file = new File(getResourcesPath(className)); putAllProperties(file); updateStorage(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 updateStorage(Object parentKeyObj, Properties properties) { updateStorage(parentKeyObj.getClass().getSimpleName(), properties); } private void updateStorage(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(); } }
Скачать исходный код примера: Selenium RC + Page Object + статические ресурсы
Обратите внимание, что в теперешней реализации мы вынесли все статические ресурсы в файлы. И теперь в случае, если какие-то данные будут изменены, например, id “UserName” изменится на “user_name” и нужно будет изменить тип локатора для поиска c “id” на “xpath”, то нам всего навсего надо будет заменить значение в файле, оставив код фреймворка без изменения:
field.username.locator = xpath field.username.arg = //input[@id=’user_name’]
Описанный выше подход при работе со статическими ресурсами, используется уже на протяжении двух лет, и значит он в полевых условиях доказал свое право на существование. Надеюсь, что после внимательного прочтения кому-то тоже захочется реализовать у себя что-то похожее, либо абсолютно такое же.
Автор: Алексей Булат