Есть отличная удаленная работа для php+codeception+jenkins+allure+docker спецов. 100% remote! Присоединиться к проекту

Page Object Pattern [пример написания своего фреймворка]

page-object
webdriver
testng
java
Теги: #<Tag:0x00007f7b62060998> #<Tag:0x00007f7b62060858> #<Tag:0x00007f7b620606c8> #<Tag:0x00007f7b62060538>

(Andrey) #1

Всем привет.
Изучаю Page Object Pattern. Написал два класса для реализации этого шаблона и проверил два тест-кейса (все работает). Хотелось бы услышать насколько то, что я написал, похоже на Page Object (правильно ли я понимаю идею), на что обратить внимание при написании, какие вопросы стояли перед вами, когда вы писали свой первый Page Object фреймворк? В общем любые советы.

Класс один GoogleTest:

package Google;

import static org.testng.Assert.assertEquals;
import org.openqa.selenium.By;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class GoogleTest extends InitialTestClass {

	private String url = "https://www.google.com/ncr";
	
	private By searchFieldLocator = By.name("q");
        private String requiredText = "Selenium";
	private By searchResultLocator = By.xpath("//div[@class='srg']//a[text()='Selenium - Web Browser Automation']");
	private String expectedResultText = "Selenium - Web Browser Automation";
	
	private By signInButton = By.xpath("//a[text()='Sign in']");
	private By emailField = By.id("identifierId");
	private String requiredEmail = "selenium@selenium.org";
	private By emailResultLocator = By.xpath("//div[contains(text(), 'find your Google Account')]");
	private String expectedEmailResultText = "Couldn't find your Google Account";
	
	@BeforeMethod
	public void setup() {
		initializeDriver();
	}
	
	@Test
	public void googleSearchText() {
		setPropertyWindow(driver);
		open(url);
		searchText(driver, searchFieldLocator, requiredText);
		assertEquals(actual(searchResultLocator), expectedResultText);
	}
	
	@Test
	public void googleSignInWrongEmail() {
		setPropertyWindow(driver);
		open(url);
		signIn(driver, signInButton);
		inputEmail(driver, emailField, requiredEmail);
		assertEquals(actual(emailResultLocator), expectedEmailResultText);
	}
	
	@AfterMethod
	public void closeDown() {
		delay(3000); //чисто для посмотреть
		driver.close();
	}
	
}

И второй класс InitialTestClass:

package Google;

import java.util.concurrent.TimeUnit;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.remote.DesiredCapabilities;


public class InitialTestClass {
	
	static WebDriver driver;
	
	public WebDriver initializeDriver() {
		DesiredCapabilities capabilitiesFirefox = new DesiredCapabilities();
		capabilitiesFirefox.setCapability("marionette", true);
		System.setProperty("webdriver.gecko.driver", "e:\\Autogecko\\Udemy\\src\\test\\resources\\Geckodriver 0.16.1\\geckodriver.exe");
		driver = new FirefoxDriver(capabilitiesFirefox);
		return driver;
	}
	
	public WebDriver setPropertyWindow(WebDriver driver) {
		driver.manage().window().maximize();
		driver.manage().timeouts().implicitlyWait(4, TimeUnit.SECONDS);
		return driver;
	}

	public void open(String url) {
		driver.get(url);
	}
	
	public void delay (long millisec) {
		try {
			Thread.sleep(millisec);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	public WebDriver searchText(WebDriver driver, By locator, String text) {
		driver.findElement(locator).sendKeys(text, Keys.ENTER);
		return driver;
	}
	
	public String actual(By locator) {
		return driver.findElement(locator).getText();
	}
	
	public WebDriver signIn(WebDriver driver, By locator) {
		driver.findElement(locator).click();
		return driver;
	}
	
	public WebDriver inputEmail(WebDriver driver, By locator, String text) {
		driver.findElement(locator).sendKeys(text, Keys.ENTER);
		return driver;
	}
	
}

Есть ощущение, что первые две строки в методах setPropertyWindow(driver); и open(url); тоже можно куда-то вынести, но, если, например, в третьем тесте, который здесь потенциально будет URL изменится, то уже и резона как бы нет…
Есть ощущения, что в классе InitialTestClass хорошо бы оставить только методы для инициализации драйвера и настройки окна браузера initializeDriver() и setPropertyWindow(), а остальные методы, которые отображают непосредственно саму реализацию методов вынести куда-то в третий класс исключительно для реализации логики методов.
После каждого теста я закрываю драйвер и для следующего теста я открываю его заново. По времени, конечно дольше, но, вроде как для чистоты теста то что надо, как мне кажется. Может не правильно?


Modern automation framework in 2018 / Создаём свой фреймворк в 2018 году
Ищу пример простого микрофреймворк и примера паттерна PageObject
QA weekly #31: музыкa, безопасноe тестирования в продакшене, опыт ускорения тестов, Astra - тестирование безопасности, PageObject
#2

Тут много всего, конечно. Но первое, что следует сделать - это завести отдельный класс для странички логина, и туда вынести все локаторы и методы для работы с этой страницей, вроде “signIn”. И в тесте или в тестовом классе создавать инстанс этой страницы и дергать её методы. Тест и тестовый класс ничего не должны “знать” про локаторы и драйвер, - только бизнес логика


(Eugene Moskalenko) #3

Вот тут можете глянуть, то что вам выше написали - https://github.com/evgmoskalenko/web-qa-java-framework, на примере поиска в Google.

Там много конечно чего переделать надо, писал тот вариант давненько, но там точно есть пейдж обжект паттерн…

Суть паттерна такая:

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

Еще очень правильно, чтобы каждый метод, возвращал вам либо текущий объект страницу, либо возвращал вам тот обьект, на какую страницу будет переход, после выполнения…

Пример:

Главная страница гугла:

public class GoogleHomePage extends BasePage<GoogleHomePage> {

    @Override
    protected String getUrl() {
        return BASE_URL.getValue();
    }

    @Step("Search '{0}'")
    public GoogleSearchResultsPage searchFor(String text) {
        $(By.name("q")).val(text).pressEnter();
        return googleSearchResultsPage();
    }

}

Страница гугла с результатами поиска:

public class GoogleSearchResultsPage extends BasePage<GoogleSearchResultsPage> {

    @Step("Get results")
    public ElementsCollection getResults() {
        return $$("#ires .g");
    }

    @Step("Get result by index '{0}'")
    public SelenideElement getResult(int index) {
        return $("#ires .g", index);
    }

}

Тест:

    @Test(groups = "google_search", priority = 10)
    public void someTest() {
        GoogleSearchResultsPage results =
                googleHomePage().openPage().searchFor("qa automation framework");

        results.getResults().find(text("Test automation - Wikipedia"));
}

Фреймворк свой писать можете как хотите. Главное, чтобы ваши тесты были как можно короче и как можно понятнее, и чтобы эти тесты можно было писать быстро и легко поддерживать…

Если обратите внимание, то я абсолютно точно знаю, что после поиска я должен попасть на новую страницу - результаты поиска… Поэтому после выполнения поиска - searchFor(), я возвращаю объект (страницу) - GoogleSearchResultsPage

Но при этом, не стоит забывать за наследование или композицию. Когда вам надо будет что-то общее, что есть на других страницах - вынести куда-то в одно место, чтобы потом переиспользовать…

К примеру если искать можно абсолютно на всех страницах вашего приложения, и элементы эти не меняются и поиск, то можно вынести метод searchFor() в родительский класс - BasePage… Чтобы его не приходилось копипастить во всех страницах и можно было использовать, и написать реализацию поиска только один раз…

И кстати, постарайтесь сделать так, чтобы в тестах не было драйвера.


(Andrey) #4

Спасибо за ответы.
Пока я немного закопался в рефакторинге своего фреймворка, но на подходе версия 2.0 с учетом ваших ответов и замечаний ))


(Andrey) #5

Вот собственно версия 2 после рефакторинга (все вроде бы работает). Схема следующая:

Класс APIClass:

package google;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

public abstract class APIClass {
	
	public abstract WebDriver getDriver();
	
	public void open(String url) {
		getDriver().get(url);
	}
	
	public WebElement find(By locator) {
		return getDriver().findElement(locator);
	}
        
	public String getActualText(By locator) {
		return find(locator).getText();
	}
        
	public void delay (long millisec) {
		try {
			Thread.sleep(millisec);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
}

Класс BaseClass:

package google;

import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.remote.DesiredCapabilities;

public class BaseClass extends APIClass {
	
	static WebDriver driver;
	
	@Override
	public WebDriver getDriver() {
		return driver;
	}
	
	@Before
	public void setUp() {
		initializeDriver();
		setPropertyWindow();
		setPropertyTimeOut();
	}
	
	@After
	public void closeDown() {
		delay(3000);
		driver.close();
	}
	
	public WebDriver initializeDriver() {
		DesiredCapabilities capabilitiesFirefox = new DesiredCapabilities();
		capabilitiesFirefox.setCapability("marionette", true);
		System.setProperty("webdriver.gecko.driver", "e:\\Autogecko\\Udemy\\src\\test\\resources\\Geckodriver 0.16.1\\geckodriver.exe");
		driver = new FirefoxDriver(capabilitiesFirefox);
		return driver;
	}
	
	public WebDriver setPropertyWindow() {
		driver.manage().window().maximize();
		return driver;
	}

	public WebDriver setPropertyTimeOut() {
		driver.manage().timeouts().implicitlyWait(4, TimeUnit.SECONDS);
		return driver;
	}
}

Класс GoogleHomePage:

package google;

import org.openqa.selenium.By;
import org.openqa.selenium.Keys;

public class GoogleHomePage extends BaseClass {
	
	private String url = "https://www.google.com/ncr";
	private By searchFieldLocator = By.name("q");
	private By signInButton = By.xpath("//a[text()='Sign in']");
	
	public GoogleHomePage openPage() {
		open(url);
		return this;
	}
	
	public GoogleSearchResultsPage searchText(String text) {
		find(searchFieldLocator).sendKeys(text, Keys.ENTER);
		return new GoogleSearchResultsPage();
	}
	
	public GoogleSignInPage signIn() {
		find(signInButton).click();
		return new GoogleSignInPage();
	}
}

Класс GoogleSearchResultsPage:

package google;

import org.openqa.selenium.By;

public class GoogleSearchResultsPage extends BaseClass {

	private By searchResultLocator = By.xpath("//div[@class='srg']//a[text()='Selenium - Web Browser Automation']");
	
	public String getActualText() {;
	 	return getActualText(searchResultLocator);
	}	
}

Класс GoogleSignInPage:

package google;

import org.openqa.selenium.By;
import org.openqa.selenium.Keys;

public class GoogleSignInPage extends BaseClass {
	
	private By emailField = By.id("identifierId");
	private By emailResultLocator = By.xpath("//div[contains(text(), 'find your Google Account')]");
	
	public GoogleSignInPage inputEmail(String text) {
		find(emailField).sendKeys(text, Keys.ENTER);
		return this;
	}
	
	public String getActualText() {;
	        return getActualText(emailResultLocator);
	}
}

Класс GoogleTest:

package google;

import static org.junit.Assert.*;
import org.junit.Test;

public class GoogleTest extends BaseClass {

	@Test 
	public void googleSearchText() {
		GoogleSearchResultsPage resultPage = new GoogleHomePage().openPage().searchText("Selenium");	
		assertEquals(resultPage.getActualText(), "Selenium - Web Browser Automation");
	}
	
	@Test
	public void googleSignInWrongEmail() {
		GoogleSignInPage signInPage = new GoogleHomePage().openPage().signIn().inputEmail("selenium@selenium.org");
		assertEquals(signInPage.getActualText(), "Couldn't find your Google Account");
	}	
}

Просьба проверить опытным взглядом то что есть и указать на ошибки ))

Вот, например, я перегрузил метод getActualText() из APIClass (или как это можно назвать, если в сигнатуре нового метода нет параметров и он вызывает себя как бы рекурсивно из суперкласса), но указать аннотацию @overload я не могу - вываливается ошибка. Хотя, я думаю,что ее нужно указать чтобы было понятно, что данный метод перегружается.
Или это переопределение? @override тоже выбрасывает ошибку…
@evgmoskalenko, вот у вас в строке googleHomePage().openPage().searchFor("qa automation framework"); например, новый объект new googleHomePage() указывается без ключевого слова new. Я немного не разобрался, почему? Я указываю у себя создание объекта явно с использованием new.


(Ramon Menezes) #6

много смотреть, то что первое бросилось в глаза, Почему клас тестов смотрит на BaseClass?


(Andrey) #7

Не знаю, мне казалось что это нормально) Нужно вводить отдельный класс, для того чтобы класс тестов наследовался в свою очередь от него?


#8

ну да, как по мне - это норм практика

По поводу дальнейших действий - для соблюдения каннонического page object, надо заменить

на просто поля типа WebElement и повесить на них анноташки FindBy (как только что обсуждалось в соседнем топике) :wink: - перебрать список веб элементов и выбрать нужное


(Andrey) #9

Насчет, отдельного класса буду думать (потому что что в него вводить-то? лишняя сущность пока для меня не понятная для чего она), а вот насчет аннотации @FindBy - пока не хочу ее использовать - не нравится она мне… Буду пилить пока не канонический Page Object )


(vmaximv) #10

Что ты перенести понятия “тест” и “страница”, которые при данной реализации существуют исключительно в вашей голове, в код.


(Andrey) #11

Просмотрел класс BaseClass - вроде все то, что нужно вынести с тестового класса тут присутствует и ничего лишнего нет. Если для того, чтобы “перенести понятия “тест” и “страница”” нужно выделить отдельный класс, значит, наверное надо добавить класс BasePage все же и от него наследовать все пейджи… А BaseClass остается. Так или нет?
И все-таки, глубинного смысла в добавлении класса пока не могу увидеть. Ну добавится класс от которого будут наследоваться пейджи и Page Object Pattern станет каноническим. А практическая польза от этого класса где и в чем она будет физически выражаться - меньше кода станет, общие методы будут и т.д. - в чем пользу можно измерить кроме теоретического утверждения “потому что так принято”?


(vmaximv) #12

Если упростить:

  1. Тесты не должны ничего знать о драйвере
  2. Пейджи не должны ничего знать о тестах

(Andrey) #13

Вы так и не ответили на мой вопрос - в чем практическая польза введения нового класса? Да, мой тестовый класс наследуется от бэйз-класса в котором есть экземпляр драйвера и теоретически он может получить к нему доступ. Но ведь он не получает. И только ради теоретической возможности нужно вводить новый класс потому что так написано в SOLID?
Кстати, есть и противоположные мнения, например, Алексей Виноградов на конференции QA Fest 2016 говорит что SOLID для тестов это зло! Ну и плюс еще парочку анти-паттернов…


Если лень смотреть полностью - можно с 29 минуты.
Много людей - много мнений. Хотелось бы получить какое-то осознанное подтверждение того или иного утверждения и видеть выгоду того или иного действия.


(vmaximv) #14

Любой уважающий себя индус, используя ваш “дву-классовый-фреймворк” версии два-ноль напишет тесты так:

public class GoogleTest extends BaseClass {

	@Test 
	public void googleSearchText() {
		initializeDriver();
		open("https://www.google.com/ncr");
		find(By.name("q").sendKeys("Selenium");			
		assertEquals(find(By.name("q").getText(), "Selenium - Web Browser Automation");
	}	
}

И еще пару “логичных” зарисовок:


new GoogleHomePage().openPage().searchText("Selenium").setPropertyWindow().quit();
new GoogleHomePage().openPage().open("");
new GoogleHomePage().openPage().initializeDriver();

(Oleksii Ihnatiuk) #15

В абстрактный класс BasePage вы выносите методы, которые дублируются у вас на страницах, например метод клик, который вы хотите переписать с умным ожиданием.
Зачем и почему класс BasePage должен содержать логику ваших тестов: что происходит перед/после каждого метода/тестового класса, то как вы инициализируете/убиваете драйвер, как вы инициализируете логгирование тестов, как вы оборачиваете драйвер в потокобезопасные конструкции. Эту логику инкапсулируют в отдельный абстрактный класс BaseTest.
Сейчас вы не понимаете данный уровень абстракции, потому что он вам не особо нужен и это нормально, но мы должны помнить, что класс должен делать/уметь/знать лишь то что ему нужно. Плюс хочу сказать что есть и более высокие уровни абстракции в построении тестов, но они могут быть вам не нужны.
Все наши выверты в построении архитектуры для тестов делаются не только для того, чтобы уменьшить дублирование кода, мы инкапсулируем логику в классах по целям, для которых они создаются.


(Andrey) #16

И что тут к чему? Мне не понятно, что вы пытаетесь сказать, упоминая индусов. Тем более про “дву-классовый-фреймворк”, когда он уже состоит из 6 классов…
Если это типа сарказма в мой огород, что я неуч безграмотный - да, пожалуйста! Я учусь и не знаю других путей изучения чего-то нового, кроме как методом проб и ошибок.
А Вы наверное, сразу родились с сокровенным знанием Page Object Patterna? )))


(Andrey) #17

По поводу BasePage. Да. у меня методы, которые дублируются вынесены в класс APIClass, а вот BaseClass, который у меня есть как раз и не содержит логику тестов, а делает то что вы описываете, только он у меня назван по другому, надо его наверное переименовать в BaseTest чтобы было привычнее для слуха. Спасибо.


(Oleksii Ihnatiuk) #18

Я могу объяснить вам так как объясняли мне.
Есть классы Pages, у них есть родитель. Как вы его назовете? Base Page или Base Class?
Класс Base Test не будет же у вас наследоваться от Base Page.


(vmaximv) #19

Вам все ошибки разжевали по нескольку раз - однако вы продолжаете упорствовать.

Я скажу вам по секрету - не существует такого паттерна Page Object впринципе, это просто прямой результат, когда детали реализации отделяются от тестов.


(Eugene Moskalenko) #20

Не совсем понимаю, вы не понимаете, почему надо:

TestClass_1 -> (наследовать от) BaseTest
TestClass_2 -> (наследовать от) BaseTest

PageClass_1 -> (наследовать от) BasePage
PageClass_2 -> (наследовать от) BasePage

Вот это вам непонятно и вы хотите наследовать так?

TestClass_1 -> (наследовать от) BaseClass
TestClass_2 -> (наследовать от) BaseClass

PageClass_1 -> (наследовать от) BaseClass
PageClass_2 -> (наследовать от) BaseClass

Если да, то почитайте зачем делается наследование.

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

Это могут быть просто методы или абстрактные методы, реализация которых будет лежать в конкретном дочернем классе своя…

Разделяй и властвуй :slight_smile: Так же не забывайте про инкапсуляцию и protected методы, поля, доступ к которым будут только у дочерних классов. Зачем же иметь доступ тестам, к тому, что должно быть в пейджах и наоборот… :slight_smile:

При расширении фреймворка легко будет потом допустить ошибку и непонятно будет, зачем оно вам надо в пейджах или тестах, что относится непосредственно к пейджам или тестам…

Относитесь не так, что это SOLID, подумайте со стороны, что потом будет тяжелее набыдлокодить и переиспользовать новому автоматизатору неправильно реализацию :slight_smile:

Абстрагируйтесь от SOLID, если вы в душе бунтарь. :slight_smile: И посмотрите на это с другой стороны :slight_smile: