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

Вот тут можете глянуть, то что вам выше написали - 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 лайк

Я понимаю, что так логично наследовать, я не понимаю утверждения, что “1. Тесты не должны ничего знать о драйвере”. Что значит не должны и как это возможно если класс тестов наследуется от класса BaseTest в котором, в частности, находится статический экземпляр драйвера…
Вы, кстати, не ответили с чего у вас там новый объект создается без ключевого слова new?
@CrispusDH, да, все это логично по названиям.

Короче, переписываю третий вариант с базовыми классами для тестов и обджектов…

P.S. Ну и опять же два моих класса BaseTest и BasePage будут наследоваться от моего абстрактного APIClass

ну я считаю, что не красиво, когда в тестах идет инициализация, я считаю, что этим должен заниматься фремворк, а в тестах должно быть все красиво и лаконично, “без лишней суеты” :slight_smile:

Посмотрите то, что скинул на гитхабе, там есть реализация, посредством которой это реализовано… :slight_smile:

А зачем им что-то знать о драйвере? :slight_smile: Драйвер - это низкоуровневая штука, пусть фреймворк ваш и занимается драйвером, а тесты у вас пусть будут, как конструктор, без всяких инициализаций и всяких там драйверов и прочей лабуды… Тесты должны писаться очень быстро, без долгого дебага… Да и понятней тест, когда в нем все просто…

ну вот так вот и возможно, в пейджах или где-то еще вы этот драйвер используете. Возможно у вас есть какой-то слой со всями там кликами и вейтами и прочей фигней, в которой драйвер используется. А сами тесты уже получаются изящные…