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


(Alex) #19

Нее я имел ввиду 500 элементов по всей админке учитывая разный Id. Поэтому очень интересует обращение к ним через тест.


(vmaximv) #20

А в чем польза этих 1000 геттеров/сеттеров? Стоит ли, так буквально, переносить все “правила хорошего тона” программирования на автоматизацию?
Пусть есть еще DTO - 500 филдов+1000 геттер/сеттер. Итого 500+1000+500+1000=3 000 филдов и методов для 500 реальных сущностей. С моей точки зрения, у автоматизации основная цель это отдача (КПД), а у этих 2000 методов она равна нулю.


(Alex) #21

Так предложите как будет задаваться обращение к полю и изменение его через сам тест? Я просто жду конкретного ответа чтобы исправить свой Framework.

P.S. Сейчас реализация например с попапами такая.

card.popup("Видеофильм", "Маруся") //попап сам определит какой это вид элемента, а также переключится на самый последний попап и применит к нему значение, в данном случае это текстовое поле

card.popup("Возраст", "+3") //это уже Dropbox

card.popup("Скидка", "on") //это Checkbox

(Sergey Korol) #22

Вместо того, чтобы “ждать конкретного ответа”, уже давно могли бы попробовать разные варианты и сделать для себя выводы. Вам уже кучу ссылок накидали с различными подходами, а вы все ждете готовенького решения.

Начнем с простого. Глядя на следующий код:

кто сможет ответить на вопрос: что собственно ваш тест тестирует? Абстрактные попапы какой-то мистической карты? Какие попапы? Алерты? Нотификейшены? Какой карты? Игральной? Гугл мэпс? Или может это профиль юзера? Тест должен отражать логику тестируемого сценария. Ваши 3 строки - это 3 компонента различных сущностей: Movie, User, Discount. Вы во всех своих примерах плюете на 4 фундаментальных концепции: OOP, DDT, PageObject, DSL. Любите функциональное программирование? Пишите на Scala. Плевать на основные концепции? Напишите свой DSL для автоматизации.

По теме:

configPage()
      .fillInMovieInfo(movie)
      .fillInUserInfo(user)
      .fillInDiscountInfo(discount)
      .submit();

Как заполнить поля на основании переданных данных - уже забота страницы, но уж никак не теста. Хотите нечто generic? Хотите избавиться от миллиона геттеров? Придумайте способ маппинга инкапсулированных элементов страницы + напишите generic verification модуль. Как это сделать? Out of scope данной темы.


(Alex) #23

О каком готовеньком решении идет речь, вы о чем вообще? Если бы внимательно прочитали тему, я твердо сказал, что мне не нужны готовые решения. Сначало остановился на Getter&Setter, но были сомнения. Я вас не прошу писать за меня код. Я прошу как новичку в программировании дать совет, что использовать (в крайнем случае что почитать)

P.S. например о геттерах и сеттерах я вообще только из этого форума узнал. Например реализация в одной из ссылок, очень интересная:

textField("Last Name").type(lastName);
    textField("Phone").type(phone);
    textField("Email").type(email);
    textField("Username").type(userName);
    textField("Password").type(password);
    textField("Password again").type(password);
    dropDownList("User Type").select(userType);
    dropDownList("Reporting Level").select(reportingLevel);
    checkbox("I agree").check();

но пока не совсем понимаю как она работает.


(Sergey Korol) #24

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

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

(Sergey Korol) #25

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

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

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


(Alex) #26

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

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

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


(Sergey Korol) #27

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


(Alex) #28

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


(Sergey Korol) #29

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


(Дмитрий Жарий) #30

Ну, как выяснилось в посте выше, всего в админке 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");

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


(Alex) #31

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


(Alex) #32

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

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

#List2Lists
users = "Alex"

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


(Дмитрий Жарий) #33

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


(Alex) #34

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

В админке 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) {
			
		}		
		
	}

(Alex) #35

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

База данных 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 это значение из базы данных поместив в него все локаторы. Но мне кажется в каждом классе будет очень громоздким описывать каждый раз массив. Есть какой либо способ пройтись по всем элементам?


(Дмитрий Жарий) #36

Привет @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


(Alex) #37

Решил сделать универсальный метод для каждой формы. Сделано это для того чтобы избавить пользователя каждый раз создавать массив 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 ");
			
		}
		
	}

(Alex) #38

Так как в админке куча форм которые наследуются от 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;
}
...
}