Про Тестинг: обеспечение качества, тестирование, автоматизация

Раздел: Автоматизация > Практикум > Автоматизированное тестирование: работа со статическими ресурсами

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

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

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

Существует 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<String, Properties> propertiesMap), ключом в котором будет являться название объекта страницы, а значением объект Properties со списком статических данных.

DataStorage.java
public 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.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);
    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.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();
    }
}

Скачать исходный код примера: Selenium RC + Page Object + статические ресурсы

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

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

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


Автор: Алексей Булат

Наверх