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

Всем привет.
Изучаю 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(), а остальные методы, которые отображают непосредственно саму реализацию методов вынести куда-то в третий класс исключительно для реализации логики методов.
После каждого теста я закрываю драйвер и для следующего теста я открываю его заново. По времени, конечно дольше, но, вроде как для чистоты теста то что надо, как мне кажется. Может не правильно?

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

Вот тут можете глянуть, то что вам выше написали - GitHub - evgmoskalenko/web-qa-java-framework: QA Automation web framework. Java. Maven. Allure., на примере поиска в 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… Чтобы его не приходилось копипастить во всех страницах и можно было использовать, и написать реализацию поиска только один раз…

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

1 лайк

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

Вот собственно версия 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.

1 лайк

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

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

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

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

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

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

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

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

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

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

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


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

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

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();
1 лайк

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

1 лайк

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

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

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

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

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

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

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:

1 лайк