Структура и организация тестов (Аннотация @Test)

Все конкретные ответы зарыты в поиске данного форума.

  • Концепции ООП.
  • PageObject:
  • Архитектура:

Реализация конкретных html elements (textField, dropDown, checkbox), у который есть свой уникальный набор методов. Доступ осуществляется по имени, а в методы передается конкретный набор данных. При этом page elements все еще являются частью page object. Т.е. они встраиваются в страницу при помощи композиции.

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

Если для вас и его мнение - не показатель, то уж извините.

Я не понимаю почему вы в посте обязательно срываетесь, вы хотите устроить флейм? Я наоборот всех отвечающих на форуме слушаю и придерживаюсь их мнения. Лучше скажите, я правильно понимаю:

textField("Last Name").type(lastName);

type(lastName) - это обращение к WebElement’у?

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

Я прочитаю и посмотрю, то что вы выше ответили. Но вопросы я скорее задавал просто от недостатка самой информации, поэтому и повторялся. Еще раз спасибо. P.S. и нет, меня не задело ваше обращение =)

type - метод объекта textField, куда вы передаете данные, которые нужно ввести.
Сам textField - универсальный элемент; не знаю, как он внутри реализован, но смею предположить, что в момент передачи стринги “Last Name”, осуществляется поиск элемента по заданному имени; либо локатор формируется динамически, либо элементы замаплены внутри. Тем самым, вы получаете возможность обратиться к элементу по ключу.

Ну, как выяснилось в посте выше, всего в админке 500 филдов, а не на отдельной странице.
Вверху поста я вижу скриншот, где я насчитал 30 полей. Тут я бы создал отдельный Data Transfer Object (MovieData) с тридцатью полями, а саму страницу описал бы как PageObject – EditMoviePage.

В EditMoviePage я бы создал два дополнительных метода:

void fillForm(MovieData data);
MovieData readForm()

которые устанавливают значения согласно данным DTO и читают эти значения соответственно.

Учитывая то, что эти два метода находятся внутри пейджобжекта, дополнительные геттеры и сеттеры на данном этапе не требуются. Они потребуются только тогда, когда нам явно будет необходимо работать с конкретными полями формы.

В класс MovieData я бы добавил статический метод getDefault(), который бы заполнял поля формы правильными рандомными данными (или как вариант строка + текущая дата). Таким образом, тест мог бы выглядеть так (псевдокод):

MovieData expectedData = MovieData.getDefault();
editMoviePage.fillForm(expectedData);

editMoviePage.save();
editMoviePage.close();

manageMoviesPage.open(expectedData.name);

MovieData actualData = editMoviePage.readForm();

verifyData(expectedData, actualData);

verifyData(expectedData, actualData);

Я тут специально не использовал asserAreEqual для двух объектов, потому что во многих фреймворках нет такого стандартного метода, который бы проверил каждое поле expected на соответствие actual.

Чтобы не проверять каждое поле отдельно мы можем переопределить метод toString() в классе MovieData, который бы возвращал строку со всеми полями объекта:

title: "Hello world" \n
rating: 5\n
...

таким образом, при помощи DTO можно проверить все 30 полей одной командой как текстовую строку.

методы работы с конкретными полями

Я считаю, что для каждого поля, с которым проводятся манипуляции напрямую из кода тестовго метода, нужно создать геттер и сеттер. Это не значит, что все 30 геттеров нужно создать сразу. Нет, лучше это сделать по мере необходимости.
Если например, я не смогу что-то сделать через MovieData, то создам для того поля отдельный сеттер.

С большими формами на > 20 полей будет много рутинной работы. Зато, с маленькими формами – такой работы будет значительно меньше. Можно обойтись и без сеттеров, но тогда в коде теста, вместо

somePage.fillName("Hello")

будет дополнительный код по типу:

somePage.name.clear();
somePage.name.sendKeys("Hello");

как по мне, лучше эти строки спрятать внутри сеттера.

2 лайка

Просто огромное спасибо за ответ. Буду пробовать сначало на маленьком примере, а потом начну переделывать свой код.

1 лайк

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

#TextField
number = "0000"
name = "0000"
...
#Checkbox
barker = "on"
OTT DVR = "on"
...
#Combobox
location = "Moscow"

#List2Lists
users = "Alex"

метод fillForm() будет считывать данные с файла. И автоматически определять метод их заполнения. Такой способ будет нормальным?

Конечно. Очень хороший подход.

Столкнулся с некоторой проблемой при заполнении формы по дефолтным значениям:

В админке 2/3 всех полей распределили по табам. 5-10 элементов доступны сразу. Остальные доступны если только нажать на необходимый таб (дополнительные настройки, изображения, регионы). Каким образом мне лучше определять, находится ли элемент в табе или нет?

P.S. сегодня создал небольшой метод который сам переключает на таб если элемент не доступен.

protected void checkTab(WebElement element){
		
		try{
			if(element.findElement(By.xpath("//ancestor::*[@class='form-sections']")).isEnabled()) // проверяем доступен ли элемент на странице
{
			String getLabel = element.findElement(By.xpath("//ancestor::section[@label]")).getAttribute("label"); // берем значение атрибута секции в которой находится элемент.

			driver.findElement(By.xpath("//*[@class='form-sections']//li/a[normalize-space(text())='"+getLabel+"']")).click();} // по getLabel переходим на нужный таб.
			
		}catch(NoSuchElementException e) {
			
		}		
		
	}

В общем в итоге получилось пока что следующее:

База данных sql (таблица и поля):

card(id,name) | field(id,name,type,locator, value) | fields_in_card(card_id,field_id)

Test:

...
@Test 
               List <Fields> expectedData = Factory.getInstance().getFieldsInCardDAO().getAllFields("3"); //тройка это id в БД определенной формы на сайте
		HomePage homepage = loginPage.loginAs(userData);
		PromoActionsMP promoActionsMP = homepage.openPromoActions(); //открываем страничку где хранится форма
		PromoActions promoActions = promoActionsMP.addNewCard(); //добавляем новую форму
		promoActions.fillForm(expectedData); //заполняем все поля по дефолту

Страница описанная в PageObject:

public class PromoActions extends Card{
	
	@FindBy(id="row.name")
	protected WebElement row_name;	
	
	@FindBy(css="[id='row.sortOrder']")
	protected WebElement row_sortOrder;
...

        HashMap<String, WebElement> element = new HashMap<>(); //создаем HashMap<String, WebElement> в который помещаем все локаторы в этом классе. Где String, берется из БД (field->locator)

        element.put("row_name", row_name);
        element.put("row_sortOrder", row_sortOrder);
...

       public void fillForm(List<Fields> getFields) {
		
		for (Fields fields : getFields){
			
			String type = fields.getType();
		
			switch(type){
			case "textfield":
				type(element.get(fields.getLocator), fields .getValue());
				break;
				default:
					break;
			
			}

Но каким образом мне пройтись по всем локаторам описанным в классе? Я подумал использовать HashMap<String, WebElement> element // где String это значение из базы данных поместив в него все локаторы. Но мне кажется в каждом классе будет очень громоздким описывать каждый раз массив. Есть какой либо способ пройтись по всем элементам?

Привет @mindjin,

Есть множество способов решения вашей задачи, и теоретически, любой способ подойдет.
Вопрос только в практической реализации.
Так что ваша идея вполне работоспособна.

В своей практике я перепробовал множество решений: начинал с HashMap’ов (или Dictionary в C#), экспериментировал с рефлексиями, которые могут достать любое поле из класса по его текстовому имени и присвоить полю значение, пытался настроить Automapper – класс, который используя набор правил, может трансформировать данные из одного класса в другой.

Картинко PHP Steet Fighter

Я остановился на промежуточном решении.
Вместо List я бы для отдельной формы использовал свой отдельный класс.
Например, для заполнения логин-формы, класс выглядел бы так:

class LoginFormData {
   string loginName;
   string password;
   Boolean rememberPassword;
}

Внутри
void fillForm(LoginFormData formData)
Я бы явно присвоил значения элементам пейджобджекта.

void fillForm(LoginFormData  formData) {
    if (formData.loginName != null) {
        this.txtLogin.sendKeys(formData.loginName);
    }
    if (formData.password != null) {
        this.txtPassword.sendKeys(formData.loginName);
    }

    this.txtPassword.chkRememberMe.Checked = formData.rememberPassword; 
}

Такой способ требует нудной работы по описанию заполнения для каждого поля в отдельности.
Зато, весь этот процесс находится под вашим контролем и, например, если вдруг логика заполнения одного поля будет зависеть от другого, вы сможете решить эту задачу поправив необходимый участок кода в конкретном месте.

Для чтения из БД, создайте отдельный метод, который бы возвращал уже заполненный объект LoginFormData

Решил сделать универсальный метод для каждой формы. Сделано это для того чтобы избавить пользователя каждый раз создавать массив WebElement. В абстрактном классе Card от которого наследуются все странички, метод выглядит следующим образом:

public void fillForm(List<Element> getElement) throws IllegalArgumentException, IllegalAccessException{
		WebElement element = null;
		
/*
* добавляем все переменные класса в массив
*/
		Field[] fs = this.getClass().getFields(); 
		
		try{

/*
* Перебор всех строк в базе данных
*/
		for (Element column : getElement){ 
			
			String type = column.getType();
			
/*
*  Из всех переменных выбираем только, те у которых тип соответствует WebElement и  *присваиваем element объект класса.
*/
			for(Field fswe : fs) 
			{
				if(fswe.getType().equals(WebElement.class)){
					
					if(fswe.getName().equals(column.getNameVariable())) {
						System.out.println(fswe.getName()+"====="+column.getName());
						element = (WebElement)fswe.get(this);
					}
				}
			}

/*
*проверяем отображается ли элемент. Если элемент не отображается находим его в табах.
*/
			
			insideTab(element); 
			
			
			switch(type){ //
			case "textfield":
				type(element, column.getValue());
				break;
			case "checkbox":
				Checkbox checkbox = new Checkbox(driver);
				checkbox.editCheckbox(element, column.getValue());
				break;
			case "date":
				type(element, column.getValue());
				break;
			case "combobox":
				Combobox combobox = new Combobox(driver);
				combobox.editCombobox(element, column.getValue());
				break;
			case "list2list":
				List2Lists list2list = new List2Lists(element);
				list2list.addValue(column.getValue());
				break;
			case "linkedlist":
				Finder finder = new Finder(driver);
				finder.add(element, column.getValue());				
				break;
				default:
					break;
			
			}
			
			
		}}catch(NullPointerException e){
			System.out.println("Not all elements of table exist in array ");
			
		}
		
	}

Так как в админке куча форм которые наследуются от Page, не правильно ли было вынести отдельный метод (getDefaultValues) в Page? Выглядело бы это следующим образом:

public abstract Page{
...
public List<Element> getDefaultValues() throws SQLException, ParseException {
		String nameClass = this.getClass().getSimpleName(); //берем имя класса которое совпадает с именем в базе данных
		List<Element> fields = Factory.getInstance().getElementDAO().getAllField(nameClass); //присваиваем List все поля доступные этому классу.
		return fields;
}
...
}

Еще раз подниму эту тему. В данный момент мои тесты выглядят таким образом:

@Listeners(ElementScreenshot.class)
public class TestKaraoke extends ConfigBase{    

    @Test
        public void saveAudioPIDofAssetsOTTWithFillForm() throws Exception{
            rndNum =RandomValues.rndNumb(999999);
            app.getNavigationHelper().openPage(Menu.KARAOKE);
            app.getHomePageHelper().addForm();    
            pageManager.karaoke
            .setName("wp_58.1_"+rndNum)
            .setExternalId(rndNum)
            .setLocations("Московский");
            pageManager.karaoke
            .addAssertsOTT()
            .setName(rndNum)
            .addAudioPID()
            .setName(rndNum)
            .setPID(rndNum)
            .createAudioPID();
            pageManager.assetsOTT.createAssetsOTT();
            pageManager.karaoke.createAndClose();
            app.getHomePageHelper().openForm("wp_58.1_"+rndNum);
            pageManager.karaoke.openAssetOTT(rndNum).openAudioPID(rndNum);
            List<Element> defaultValuesAudioPID = app.getDataHelper().getRandomValues(Form.AUDIOPID);
            app.getFormHelper().fillForm(defaultValuesAudioPID, Form.AUDIOPID);
            pageManager.audioPID.saveAudioPID();
            pageManager.assetsOTT.saveAssetsOTT();
            pageManager.karaoke.saveAndClose();
            app.getHomePageHelper().openForm("wp_58.1_"+rndNum);
            pageManager.karaoke.openAssetOTT(rndNum).openAudioPID(ElementsForm.NAME.getValue());
            app.getFormHelper().verifyForm(defaultValuesAudioPID, Form.AUDIOPID);
            pageManager.audioPID.closeAudioPID();
            pageManager.assetsOTT.closeAssetsOTT();
            app.getFormHelper().deleteCard(Form.KARAOKE);    
            }
        }

Если нужно будет пояснение по поводу кода скажите, но в целом тест выглядит нормально? Спасибо

  • Что тестирует ваш тест?
  • Data hardcoding - это плохо, очень плохо. Почему не используете DataProvider?
  • Рандомайзеры / листы и т.п. никак не относятся к тестовой логике и в целом создают никому не нужный шум.
  • Данные, состоящие сугубо из random numbers, не представляют никакой ценности в тестировании. Как же логика, классы эквивалентности и т.п.?
  • Не совсем очевиден архитектурный подход реализации PageObjects через pageManager. Как устроен этот класс?
  • Статические импорты улучшат читабельность.
  • Не совсем понятна система верификейшенов. Что представляет из себя FormHelper?
1 лайк

А вы распечатайте его на бумаге, подойдите к любому мануальщику - пусть он его “прочитает”. Если сумеет понять что в нем происходит - значит тест “выглядит нормально” :wink:
Лично я тоже ничего не понял.

2 лайка

Пока что как таковых тестов нет. Это просто пока проверки работоспособности framework.

  • Конкретный тест, проверяет изменение полей на форме караок, при
    сохранении.
  • Насчет DataProvider, не пробовал пока что мало опыта, попробую.
    (Имеется ввиду @DataProvader?). Под data harcoder имеется ввиду прописывание данных внутри кода?
  • Рандомные значения использовал согласно комментарию в посте
    выше:

В класс MovieData я бы добавил статический метод getDefault(), который
бы заполнял поля формы правильными рандомными данными (или как вариант
строка + текущая дата). Таким образом, тест мог бы выглядеть так
(псевдокод):

  • PageManager подсмотрел у Баранцева. Этот класс содержит настройки
    драйвера и ссылки на “помощников”, которые определяют логику тестов.
    В своем варианте, я создал нескольких помощников, которые выполняют
    общие действия для всех страниц.

  • Статические импорты - где их именно нужно добавить? (Form и Menu это enum)

  • FormHelper содержит метод для проверки всех полей в соответствии со
    значениями в SQL таблице.

P.S. если честно я уже запутался, 3 раз переделываю Framework и вижу, что постоянно не правильно. Мне кажется за целый год я вообще ничему не научился и ничего не понимаю в программировании.

Подобные тесты должны писаться языком, понятным любому человеку. Достигается это при помощи собственного DSL, являющегося частью ваших PageObjects. Ваш тест же слишком зашумлен абсолютно неотносящейся к бизнес логике информацией. Все эти рандомайзеры и прочие технические вещи должны сокрываться на более низких уровнях абстракции, дабы улучшить читабельность.

Да и да.

Ключевое слово выделено. А теперь взгляните на это:

С каких пор name, состоящие из рандомных цифр, считаются валидными данными? Да, технически то вы можете ввести туда все, что угодно, но вы ведь QA. Разве при ручном тестировании вы бы вводили такие же значения?

Мне кажется, что вы пытаетесь совместить техническую составляющую с доменной. Драйвер вообще никак не должен быть связан с вашими пейджами. Общие действия как правило “вшиваются” в 1 BasePage класс, у которого есть доступ к драйверу. Все наследники не должны иметь ничего общего с драйвером. Это разные уровни абстракции.

Вообще, в идеале, раз уж вы оперируете термином PageObject, то структура теста не должна содержать ничего подобного:

app.getFormHelper().fillForm(defaultValuesAudioPID, Form.AUDIOPID);
pageManager.audioPID.saveAudioPID();
pageManager.assetsOTT.saveAssetsOTT();
pageManager.karaoke.saveAndClose();

Какие-то менеджеры с публичными ссылками… Все, что действительно важно для теста, это:

.fillForm(defaultValuesAudioPID, Form.AUDIOPID);
.saveAudioPID();
.saveAssetsOTT();
.saveAndClose();

Остальное - шум.

Т.е. вы ассерты засунули в пейджи?